Mercurial Hosting > lang
annotate src/lib/claude/Ai_chat.luan @ 79:d4473741142c
chat UI
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Tue, 02 Sep 2025 16:33:45 -0600 |
parents | 2be9ea450de3 |
children |
rev | line source |
---|---|
5 | 1 local Luan = require "luan:Luan.luan" |
2 local error = Luan.error | |
6 | 3 local ipairs = Luan.ipairs or error() |
38 | 4 local pairs = Luan.pairs or error() |
6 | 5 local type = Luan.type or error() |
6 local String = require "luan:String.luan" | |
7 local starts_with = String.starts_with or error() | |
8 local Html = require "luan:Html.luan" | |
9 local html_encode = Html.encode or error() | |
10 local Parsers = require "luan:Parsers.luan" | |
11 local json_parse = Parsers.json_parse or error() | |
12 local json_string = Parsers.json_string or error() | |
71 | 13 local Thread = require "luan:Thread.luan" |
76 | 14 local Claude = require "site:/lib/claude/Claude.luan" |
6 | 15 local claude_chat = Claude.chat or error() |
74 | 16 local Utils = require "site:/lib/Utils.luan" |
17 local deep_copy = Utils.deep_copy or error() | |
6 | 18 local Logging = require "luan:logging/Logging.luan" |
19 | 19 local logger = Logging.logger "claude/Ai_chat" |
5 | 20 |
21 | |
19 | 22 local Ai_chat = {} |
5 | 23 |
19 | 24 function Ai_chat.output_system_prompt(thread) |
9 | 25 thread = json_parse(thread) |
77 | 26 local system_prompt = thread.system or error() |
9 | 27 system_prompt = html_encode(system_prompt) |
28 %><%=system_prompt%><% | |
29 end | |
30 | |
52 | 31 function Ai_chat.output_messages_html(assistant_controls,thread,old_thread) |
9 | 32 thread = json_parse(thread) |
77 | 33 local messages = thread.messages or error() |
13 | 34 local n = 0 |
35 if old_thread ~= nil then | |
36 old_thread = json_parse(old_thread) | |
77 | 37 local old_messages = old_thread.messages or error() |
13 | 38 n = #old_messages |
39 end | |
40 for i, message in ipairs(messages) do | |
41 if i <= n then | |
42 continue | |
43 end | |
6 | 44 local role = message.role or error() |
45 local function output(text) | |
9 | 46 text = html_encode(text) |
79 | 47 if role=="user" then |
6 | 48 %> |
79 | 49 <div role="user" msg="<%=i%>"> |
50 <div flex><div bubble> | |
51 <div message markdown><%=text%></div> | |
52 </div></div> | |
53 </div> | |
6 | 54 <% |
79 | 55 elseif role=="assistant" then |
56 %> | |
57 <div role="assistant" msg="<%=i%>"> | |
58 <div flex><div bubble> | |
59 <div message markdown><%=text%></div> | |
60 <div trans=needed>Translating...</div> | |
61 </div></div> | |
62 <%= assistant_controls %> | |
63 </div> | |
64 <% | |
65 else error(role) end | |
6 | 66 end |
67 local content = message.content or error() | |
68 if type(content) == "string" then | |
69 output(content) | |
70 else | |
71 for _, part in ipairs(content) do | |
72 if part.type=="text" then | |
73 local text = part.text or error() | |
74 output(text) | |
75 end | |
76 end | |
77 end | |
78 end_for | |
5 | 79 end |
80 | |
54 | 81 local function get_chat(chat_id) |
82 local Chat = require "site:/lib/Chat.luan" | |
83 local User = require "site:/lib/User.luan" | |
84 local chat = Chat.get_by_id(chat_id) or error() | |
85 local user = User.current() | |
86 local is_owner = user ~= nil and user.id == chat.user_id | |
87 is_owner or not chat.is_private or error "private" | |
88 return chat | |
89 end | |
90 | |
38 | 91 local functions = { |
54 | 92 get_chat = { |
38 | 93 tool = { |
54 | 94 description = "Get the contents of a chat/thread with Claude on this website. The contents will be JSON in the format of the Claude API." |
38 | 95 input_schema = { |
96 type = "object" | |
97 properties = { | |
54 | 98 chat_id = { |
99 description = "The ID of the chat" | |
100 type = "integer" | |
38 | 101 } |
102 } | |
103 } | |
104 } | |
105 fn = function(input) | |
54 | 106 local chat_id = input.chat_id or error() |
107 local chat = get_chat(chat_id) | |
38 | 108 return chat.ai_thread or error() |
109 end | |
110 } | |
54 | 111 get_tts_instructions = { |
112 tool = { | |
69 | 113 description = "Get the text-to-speech instructions of a chat/thread on this website. These instructions are passed to OpenAI. If there are no instructions, the empty string is returned." |
54 | 114 input_schema = { |
115 type = "object" | |
116 properties = { | |
117 chat_id = { | |
118 description = "The ID of the chat" | |
119 type = "integer" | |
120 } | |
121 } | |
122 } | |
123 } | |
124 fn = function(input) | |
125 local chat_id = input.chat_id or error() | |
126 local chat = get_chat(chat_id) | |
127 return chat.tts_instructions or error() | |
128 end | |
129 } | |
69 | 130 get_stt_prompt = { |
131 tool = { | |
132 description = "Get the speech-to-text prompt of a chat/thread on this website. This prompt is passed to OpenAI. If there is no prompt, the empty string is returned." | |
133 input_schema = { | |
134 type = "object" | |
135 properties = { | |
136 chat_id = { | |
137 description = "The ID of the chat" | |
138 type = "integer" | |
139 } | |
140 } | |
141 } | |
142 } | |
143 fn = function(input) | |
144 local chat_id = input.chat_id or error() | |
145 local chat = get_chat(chat_id) | |
146 return chat.stt_prompt or error() | |
147 end | |
148 } | |
38 | 149 } |
150 local tools = {nil} | |
151 for name, f in pairs(functions) do | |
152 f.name = name | |
153 f.tool.name = name | |
154 tools[#tools+1] = f.tool | |
155 end | |
9 | 156 |
35
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
157 function Ai_chat.init(system_prompt) |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
158 local thread = { |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
159 system = system_prompt |
38 | 160 tools = tools |
35
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
161 messages = {nil} |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
162 } |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
163 return json_string(thread) |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
164 end |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
165 |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
166 function Ai_chat.has_messages(thread) |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
167 thread = json_parse(thread) |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
168 return #thread.messages > 0 |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
169 end |
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
170 |
74 | 171 local function chit_chat(thread) |
172 thread = deep_copy(thread) | |
173 local messages = thread.messages or error() | |
174 for _, message in ipairs(messages) do | |
175 local content = message.content or error() | |
176 if type(content) == "string" then | |
177 content = {{ | |
178 type = "text" | |
179 text = content | |
180 }} | |
181 message.content = content | |
182 end | |
183 end | |
184 local content = messages[#messages].content or error() | |
185 content[#content].cache_control = { type = "ephemeral" } | |
186 return claude_chat(thread) | |
187 end | |
188 | |
71 | 189 local function ask(thread) |
77 | 190 local messages = thread.messages or error() |
6 | 191 --[=[ |
192 messages[#messages+1] = { | |
193 role = "assistant" | |
194 content = [[ | |
13 | 195 hello |
6 | 196 ]] |
197 } | |
71 | 198 Thread.sleep(2000) |
6 | 199 if true then |
25 | 200 return |
6 | 201 end |
202 --]=] | |
20 | 203 -- logger.info(json_string(thread)) |
74 | 204 local resultJson = chit_chat(thread) |
6 | 205 local result = json_parse(resultJson) |
206 -- logger.info(json_string(result)) | |
207 result.type == "message" or error() | |
208 result.role == "assistant" or error() | |
209 local content = result.content or error() | |
210 messages[#messages+1] = { | |
211 role = "assistant" | |
212 content = content | |
213 } | |
38 | 214 local stop_reason = result.stop_reason or error() |
215 if stop_reason == "end_turn" then | |
216 -- ok | |
217 elseif stop_reason == "tool_use" then | |
218 local response = {nil} | |
219 for _, part in ipairs(content) do | |
220 if part.type == "tool_use" then | |
221 local f = functions[part.name] or error() | |
222 local input = part.input or error() | |
223 response[#response+1] = { | |
224 type = "tool_result" | |
225 tool_use_id = part.id or error() | |
226 content = f.fn(input) | |
227 } | |
228 end | |
229 end | |
71 | 230 messages[#messages+1] = { |
231 role = "user" | |
232 content = response | |
233 } | |
234 ask(thread) | |
38 | 235 else |
236 error(stop_reason) | |
237 end | |
238 end | |
239 | |
71 | 240 function Ai_chat.add(thread,input) |
38 | 241 thread = json_parse(thread) |
77 | 242 local messages = thread.messages or error() |
71 | 243 messages[#messages+1] = { |
244 role = "user" | |
245 content = input | |
246 } | |
247 return json_string(thread) | |
248 end | |
249 | |
250 function Ai_chat.respond(thread) | |
251 thread = json_parse(thread) | |
252 ask(thread) | |
9 | 253 return json_string(thread) |
5 | 254 end |
255 | |
19 | 256 return Ai_chat |