Initial.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
-d:ssl
|
||||
+171
@@ -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()
|
||||
Reference in New Issue
Block a user