diff options
| author | Adam Malczewski <[email protected]> | 2026-03-24 13:44:52 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-03-24 13:44:52 +0900 |
| commit | 5a44a97111d304945bbfc3da02d29a83191d816c (patch) | |
| tree | a1e31b76db2a0b0e84c5745127a0d05ddc574ec7 /src/ollama-client.ts | |
| parent | bb543c3f7840f2a3fa1b7a1fb32245fa87a30f7b (diff) | |
| download | ai-pulse-obsidian-plugin-5a44a97111d304945bbfc3da02d29a83191d816c.tar.gz ai-pulse-obsidian-plugin-5a44a97111d304945bbfc3da02d29a83191d816c.zip | |
Add initial ai tool system, and 2 tools to explore
Diffstat (limited to 'src/ollama-client.ts')
| -rw-r--r-- | src/ollama-client.ts | 139 |
1 files changed, 117 insertions, 22 deletions
diff --git a/src/ollama-client.ts b/src/ollama-client.ts index 377d640..91bd40c 100644 --- a/src/ollama-client.ts +++ b/src/ollama-client.ts @@ -1,8 +1,31 @@ import { requestUrl } from "obsidian"; +import type { App } from "obsidian"; +import type { OllamaToolDefinition } from "./tools"; +import { findToolByName } from "./tools"; export interface ChatMessage { - role: "system" | "user" | "assistant"; + role: "system" | "user" | "assistant" | "tool"; content: string; + tool_calls?: ToolCallResponse[]; + tool_name?: string; +} + +export interface ToolCallResponse { + type?: string; + function: { + index?: number; + name: string; + arguments: Record<string, unknown>; + }; +} + +export interface ToolCallEvent { + toolName: string; + friendlyName: string; + summary: string; + resultSummary: string; + args: Record<string, unknown>; + result: string; } export async function testConnection(ollamaUrl: string): Promise<string> { @@ -64,34 +87,106 @@ export async function listModels(ollamaUrl: string): Promise<string[]> { } } +/** + * Send a chat message with optional tool-calling agent loop. + * When tools are provided, the function handles the multi-turn tool + * execution loop automatically and calls onToolCall for each invocation. + */ export async function sendChatMessage( ollamaUrl: string, model: string, messages: ChatMessage[], + tools?: OllamaToolDefinition[], + app?: App, + onToolCall?: (event: ToolCallEvent) => void, ): Promise<string> { - try { - const response = await requestUrl({ - url: `${ollamaUrl}/api/chat`, - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model, messages, stream: false }), - }); + const maxIterations = 10; + let iterations = 0; - const message = (response.json as Record<string, unknown>).message; - if ( - typeof message === "object" && - message !== null && - "content" in message && - typeof (message as Record<string, unknown>).content === "string" - ) { - return (message as Record<string, unknown>).content as string; - } + const workingMessages = messages.map((m) => ({ ...m })); - throw new Error("Unexpected response format: missing message content."); - } catch (err: unknown) { - if (err instanceof Error) { - throw new Error(`Chat request failed: ${err.message}`); + while (iterations < maxIterations) { + iterations++; + + try { + const body: Record<string, unknown> = { + model, + messages: workingMessages, + stream: false, + }; + + if (tools !== undefined && tools.length > 0) { + body.tools = tools; + } + + const response = await requestUrl({ + url: `${ollamaUrl}/api/chat`, + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + const messageObj = (response.json as Record<string, unknown>).message; + if (typeof messageObj !== "object" || messageObj === null) { + throw new Error("Unexpected response format: missing message."); + } + + const msg = messageObj as Record<string, unknown>; + const content = typeof msg.content === "string" ? msg.content : ""; + const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls as ToolCallResponse[] : []; + + // If no tool calls, return the final content + if (toolCalls.length === 0) { + return content; + } + + // Append assistant message with tool_calls to working history + const assistantMsg: ChatMessage = { + role: "assistant", + content, + tool_calls: toolCalls, + }; + workingMessages.push(assistantMsg); + + // Execute each tool call and append results + if (app === undefined) { + throw new Error("App reference required for tool execution."); + } + + for (const tc of toolCalls) { + const fnName = tc.function.name; + const fnArgs = tc.function.arguments; + const toolEntry = findToolByName(fnName); + + let result: string; + if (toolEntry === undefined) { + result = `Error: Unknown tool "${fnName}".`; + } else { + result = await toolEntry.execute(app, fnArgs); + } + + if (onToolCall !== undefined) { + const friendlyName = toolEntry !== undefined ? toolEntry.friendlyName : fnName; + const summary = toolEntry !== undefined ? toolEntry.summarize(fnArgs) : `Called ${fnName}`; + const resultSummary = toolEntry !== undefined ? toolEntry.summarizeResult(result) : ""; + onToolCall({ toolName: fnName, friendlyName, summary, resultSummary, args: fnArgs, result }); + } + + workingMessages.push({ + role: "tool", + tool_name: fnName, + content: result, + }); + } + + // Loop continues — model sees tool results + } catch (err: unknown) { + if (err instanceof Error) { + throw new Error(`Chat request failed: ${err.message}`); + } + throw new Error("Chat request failed: unknown error."); } - throw new Error("Chat request failed: unknown error."); } + + throw new Error("Tool calling loop exceeded maximum iterations."); } |
