Mercurial Hosting > lang
annotate src/lib/ai/claude/Ai_chat.luan @ 75:b96cf27e719d default tip
minor
| author | Franklin Schmidt <fschmidt@gmail.com> |
|---|---|
| date | Thu, 28 Aug 2025 14:36:03 -0600 |
| parents | 64e35a92d163 |
| 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" |
| 6 | 14 local Claude = require "site:/lib/ai/claude/Claude.luan" |
| 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) |
| 26 local system_prompt = thread.system or error | |
| 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) |
| 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) | |
| 37 local old_messages = old_thread.messages or error | |
| 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 who | |
| 46 if role=="assistant" then | |
| 47 who = "Claude" | |
| 48 elseif role=="user" then | |
| 49 who = "You" | |
| 50 else | |
| 51 error(role) | |
| 52 end | |
| 53 local function output(text) | |
| 9 | 54 text = html_encode(text) |
| 6 | 55 %> |
| 74 | 56 <div role="<%=role%>" msg="<%=i%>"> |
| 57 <h3><%=who%></h3> | |
| 34 | 58 <div message markdown><%=text%></div> |
| 59 <% if role=="assistant" then %> | |
| 52 | 60 <%= assistant_controls %> |
| 34 | 61 <% end %> |
| 62 </div> | |
| 6 | 63 <% |
| 64 end | |
| 65 local content = message.content or error() | |
| 66 if type(content) == "string" then | |
| 67 output(content) | |
| 68 else | |
| 69 for _, part in ipairs(content) do | |
| 70 if part.type=="text" then | |
| 71 local text = part.text or error() | |
| 72 output(text) | |
| 73 end | |
| 74 end | |
| 75 end | |
| 76 end_for | |
| 5 | 77 end |
| 78 | |
| 54 | 79 local function get_chat(chat_id) |
| 80 local Chat = require "site:/lib/Chat.luan" | |
| 81 local User = require "site:/lib/User.luan" | |
| 82 local chat = Chat.get_by_id(chat_id) or error() | |
| 83 local user = User.current() | |
| 84 local is_owner = user ~= nil and user.id == chat.user_id | |
| 85 is_owner or not chat.is_private or error "private" | |
| 86 return chat | |
| 87 end | |
| 88 | |
| 38 | 89 local functions = { |
| 54 | 90 get_chat = { |
| 38 | 91 tool = { |
| 54 | 92 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 | 93 input_schema = { |
| 94 type = "object" | |
| 95 properties = { | |
| 54 | 96 chat_id = { |
| 97 description = "The ID of the chat" | |
| 98 type = "integer" | |
| 38 | 99 } |
| 100 } | |
| 101 } | |
| 102 } | |
| 103 fn = function(input) | |
| 54 | 104 local chat_id = input.chat_id or error() |
| 105 local chat = get_chat(chat_id) | |
| 38 | 106 return chat.ai_thread or error() |
| 107 end | |
| 108 } | |
| 54 | 109 get_tts_instructions = { |
| 110 tool = { | |
| 69 | 111 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 | 112 input_schema = { |
| 113 type = "object" | |
| 114 properties = { | |
| 115 chat_id = { | |
| 116 description = "The ID of the chat" | |
| 117 type = "integer" | |
| 118 } | |
| 119 } | |
| 120 } | |
| 121 } | |
| 122 fn = function(input) | |
| 123 local chat_id = input.chat_id or error() | |
| 124 local chat = get_chat(chat_id) | |
| 125 return chat.tts_instructions or error() | |
| 126 end | |
| 127 } | |
| 69 | 128 get_stt_prompt = { |
| 129 tool = { | |
| 130 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." | |
| 131 input_schema = { | |
| 132 type = "object" | |
| 133 properties = { | |
| 134 chat_id = { | |
| 135 description = "The ID of the chat" | |
| 136 type = "integer" | |
| 137 } | |
| 138 } | |
| 139 } | |
| 140 } | |
| 141 fn = function(input) | |
| 142 local chat_id = input.chat_id or error() | |
| 143 local chat = get_chat(chat_id) | |
| 144 return chat.stt_prompt or error() | |
| 145 end | |
| 146 } | |
| 38 | 147 } |
| 148 local tools = {nil} | |
| 149 for name, f in pairs(functions) do | |
| 150 f.name = name | |
| 151 f.tool.name = name | |
| 152 tools[#tools+1] = f.tool | |
| 153 end | |
| 9 | 154 |
|
35
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
155 function Ai_chat.init(system_prompt) |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
156 local thread = { |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
157 system = system_prompt |
| 38 | 158 tools = tools |
|
35
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
159 messages = {nil} |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
160 } |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
161 return json_string(thread) |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
162 end |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
163 |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
164 function Ai_chat.has_messages(thread) |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
165 thread = json_parse(thread) |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
166 return #thread.messages > 0 |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
167 end |
|
3117876debca
ai_first_message in textarea
Franklin Schmidt <fschmidt@gmail.com>
parents:
34
diff
changeset
|
168 |
| 74 | 169 local function chit_chat(thread) |
| 170 thread = deep_copy(thread) | |
| 171 local messages = thread.messages or error() | |
| 172 for _, message in ipairs(messages) do | |
| 173 local content = message.content or error() | |
| 174 if type(content) == "string" then | |
| 175 content = {{ | |
| 176 type = "text" | |
| 177 text = content | |
| 178 }} | |
| 179 message.content = content | |
| 180 end | |
| 181 end | |
| 182 local content = messages[#messages].content or error() | |
| 183 content[#content].cache_control = { type = "ephemeral" } | |
| 184 return claude_chat(thread) | |
| 185 end | |
| 186 | |
| 71 | 187 local function ask(thread) |
| 9 | 188 local messages = thread.messages or error |
| 6 | 189 --[=[ |
| 190 messages[#messages+1] = { | |
| 191 role = "assistant" | |
| 192 content = [[ | |
| 13 | 193 hello |
| 6 | 194 ]] |
| 195 } | |
| 71 | 196 Thread.sleep(2000) |
| 6 | 197 if true then |
| 25 | 198 return |
| 6 | 199 end |
| 200 --]=] | |
| 20 | 201 -- logger.info(json_string(thread)) |
| 74 | 202 local resultJson = chit_chat(thread) |
| 6 | 203 local result = json_parse(resultJson) |
| 204 -- logger.info(json_string(result)) | |
| 205 result.type == "message" or error() | |
| 206 result.role == "assistant" or error() | |
| 207 result.stop_reason == "end_turn" or result.stop_reason == "tool_use" or error() | |
| 208 local content = result.content or error() | |
| 209 messages[#messages+1] = { | |
| 210 role = "assistant" | |
| 211 content = content | |
| 212 } | |
| 38 | 213 local stop_reason = result.stop_reason or error() |
| 214 if stop_reason == "end_turn" then | |
| 215 -- ok | |
| 216 elseif stop_reason == "tool_use" then | |
| 217 local response = {nil} | |
| 218 for _, part in ipairs(content) do | |
| 219 if part.type == "tool_use" then | |
| 220 local f = functions[part.name] or error() | |
| 221 local input = part.input or error() | |
| 222 response[#response+1] = { | |
| 223 type = "tool_result" | |
| 224 tool_use_id = part.id or error() | |
| 225 content = f.fn(input) | |
| 226 } | |
| 227 end | |
| 228 end | |
| 71 | 229 messages[#messages+1] = { |
| 230 role = "user" | |
| 231 content = response | |
| 232 } | |
| 233 ask(thread) | |
| 38 | 234 else |
| 235 error(stop_reason) | |
| 236 end | |
| 237 end | |
| 238 | |
| 71 | 239 function Ai_chat.add(thread,input) |
| 38 | 240 thread = json_parse(thread) |
| 71 | 241 local messages = thread.messages or error |
| 242 messages[#messages+1] = { | |
| 243 role = "user" | |
| 244 content = input | |
| 245 } | |
| 246 return json_string(thread) | |
| 247 end | |
| 248 | |
| 249 function Ai_chat.respond(thread) | |
| 250 thread = json_parse(thread) | |
| 251 ask(thread) | |
| 9 | 252 return json_string(thread) |
| 5 | 253 end |
| 254 | |
| 19 | 255 return Ai_chat |
