This commit is contained in:
2026-03-03 20:17:52 +01:00
commit 854045cede
7 changed files with 1135 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
-d:ssl
+171
View File
@@ -0,0 +1,171 @@
# retoor <retoor@molodetz.nl>
import std/[asyncdispatch, json, os, strutils, sets, tables, random, strformat, logging]
import sneknim/[constants, types, config, color, rpc, router, deepseek, context, tools]
var
botConfig: BotConfig
rpcClient: SnekRpc
toolRegistry: ToolRegistry
contexts: Table[string, ChannelContext]
joinedChannels: HashSet[string]
channelMap: Table[string, ChatChannel]
proc getChannelTag(channelUid: string): string =
if channelMap.hasKey(channelUid):
return channelMap[channelUid].tag
return ""
proc getOrCreateContext(channelUid: string): ChannelContext =
if not contexts.hasKey(channelUid):
contexts[channelUid] = newChannelContext(channelUid, botConfig.systemMessage)
return contexts[channelUid]
proc sendProgress(channelUid, text: string, isFinal: bool): Future[void] {.async.} =
await rpcClient.sendMessage(channelUid, text, isFinal)
proc handleMessage(username, userNick, channelUid, message: string) {.async.} =
let action = classifyWithJoined(
username, message,
rpcClient.username, rpcClient.nick,
channelUid, joinedChannels,
getChannelTag(channelUid)
)
debug("Message action: " & $action & " from " & username & " in " & channelUid)
case action
of Ignore:
return
of RespondPing:
let rest = message[4 .. ^1].strip()
await rpcClient.sendMessage(channelUid, "pong " & rest, true)
of HandleJoin:
joinedChannels.incl(channelUid)
info("Joined channel: " & channelUid)
await rpcClient.sendMessage(channelUid, "Joined channel.", true)
of HandleLeave:
joinedChannels.excl(channelUid)
info("Left channel: " & channelUid)
await rpcClient.sendMessage(channelUid, "Left channel.", true)
of RespondChat:
try:
asyncCheck rpcClient.setTyping(channelUid, randomBrightColor())
let ctx = getOrCreateContext(channelUid)
ctx.addUserMessage(username, message)
var depth = 0
var repeatedCalls: Table[string, int]
var consecutiveErrors = 0
while depth < MaxToolCallDepth:
inc depth
debug(fmt"Tool call iteration {depth}/{MaxToolCallDepth} in {channelUid}")
asyncCheck rpcClient.setTyping(channelUid, randomBrightColor())
let payload = ctx.buildPayload()
let toolDefs = toolRegistry.getDefinitions()
let resp = await chatCompletion(payload, toolDefs)
if resp.hasKey("tool_calls") and resp["tool_calls"].kind == JArray and resp["tool_calls"].len > 0:
let assistantContent = resp{"content"}.getStr()
ctx.addAssistantToolCalls(assistantContent, resp["tool_calls"])
let toolCallCount = resp["tool_calls"].len
debug(fmt"Received {toolCallCount} tool calls")
for tc in resp["tool_calls"]:
let tcId = tc["id"].getStr()
let funcName = tc["function"]["name"].getStr()
let funcArgs = try: parseJson(tc["function"]["arguments"].getStr())
except: newJObject()
let callKey = funcName & ":" & $funcArgs
discard repeatedCalls.mgetOrPut(callKey, 0)
inc repeatedCalls[callKey]
if repeatedCalls[callKey] > MaxRepeatedToolCalls:
warn("Tool " & funcName & " exceeded max repeated calls")
ctx.addToolResult(tcId, funcName, "Error: tool called too many times with same arguments")
continue
try:
let toolResult = await toolRegistry.execute(funcName, funcArgs, channelUid)
debug("Tool " & funcName & " result length: " & $toolResult.len)
ctx.addToolResult(tcId, funcName, toolResult)
consecutiveErrors = 0
except CatchableError as e:
error("Tool " & funcName & " failed: " & e.msg)
ctx.addToolResult(tcId, funcName, "Error: " & e.msg)
inc consecutiveErrors
if consecutiveErrors >= MaxConsecutiveErrors:
break
if consecutiveErrors >= MaxConsecutiveErrors:
error("Max consecutive tool errors reached in " & channelUid)
break
continue
let content = resp{"content"}.getStr()
if content.len > 0:
let sanitized = sanitizeBotNames(content)
ctx.addAssistantMessage(sanitized)
await rpcClient.sendMessage(channelUid, sanitized, true)
break
except CatchableError as e:
error("handleMessage failed in " & channelUid & ": " & e.msg)
try:
await rpcClient.sendMessage(channelUid, "An error occurred while processing your message.", true)
except CatchableError:
discard
proc main() {.async.} =
randomize()
let logger = newConsoleLogger(lvlDebug, "[$datetime] $levelname: ")
addHandler(logger)
let configPath = if paramCount() >= 1:
let arg = paramStr(1)
if arg == "--config" and paramCount() >= 2: paramStr(2)
else: arg
else:
"config.json"
botConfig = loadConfig(configPath)
initDeepseek()
rpcClient = newSnekRpc()
toolRegistry = newToolRegistry()
toolRegistry.sendProgress = sendProgress
toolRegistry.registerDefaultTools()
contexts = initTable[string, ChannelContext]()
joinedChannels = initHashSet[string]()
channelMap = initTable[string, ChatChannel]()
info("Connecting to Snek...")
await rpcClient.connect()
asyncCheck rpcClient.receiveLoop(botConfig.username, botConfig.password)
asyncCheck rpcClient.startHeartbeat()
info("Logging in as " & botConfig.username & "...")
await rpcClient.login(botConfig.username, botConfig.password)
await rpcClient.getUser()
info("Authenticated as " & rpcClient.username & " (" & rpcClient.nick & ")")
await rpcClient.getChannels()
for ch in rpcClient.channels:
channelMap[ch.uid] = ch
info(fmt"Loaded {rpcClient.channels.len} channels")
rpcClient.onMessage = proc(username, userNick, channelUid, message: string): Future[void] {.async.} =
await handleMessage(username, userNick, channelUid, message)
info("Bot is running. Listening for messages...")
while true:
await sleepAsync(60_000)
when isMainModule:
waitFor main()