summaryrefslogtreecommitdiffhomepage
path: root/src/ollama-client.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-03-24 13:44:52 +0900
committerAdam Malczewski <[email protected]>2026-03-24 13:44:52 +0900
commit5a44a97111d304945bbfc3da02d29a83191d816c (patch)
treea1e31b76db2a0b0e84c5745127a0d05ddc574ec7 /src/ollama-client.ts
parentbb543c3f7840f2a3fa1b7a1fb32245fa87a30f7b (diff)
downloadai-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.ts139
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.");
}