Add conditional web tools to LLM agent

This commit is contained in:
2026-05-03 00:55:56 +09:00
parent 82f98ceb07
commit b28f163217
3 changed files with 185 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
import type { AppConfig } from "../config.js";
import type { Logger } from "../logger.js";
import { webFetch, webSearch } from "./web-tools.js";
interface OllamaChatMessage {
role: "system" | "user" | "assistant";
@@ -42,7 +43,7 @@ interface OllamaToolResultMessage {
}
const SYSTEM_PROMPT =
"너는 한국어로 짧고 자연스럽게 답하는 로컬 음성 비서다. 사용자의 말에 바로 답하고, 군더더기 없는 1~3문장으로 답해라. 정확한 시간, 설정 확인, 계산이 필요하면 도구를 우선 사용해라. 너는 도구 호출 루프 안에 있으며 필요하면 여러 번 도구를 호출할 수 있다.";
"너는 한국어로 짧고 자연스럽게 답하는 로컬 음성 비서다. 사용자의 말에 바로 답하고, 군더더기 없는 1~3문장으로 답해라. 정확한 시간, 설정 확인, 계산이 필요하면 도구를 우선 사용해라. 최신 정보, 오늘/최근 정보, 뉴스, 검색 요청, 사실 확인, 외부 웹페이지 내용이 필요한 경우에만 web_search 와 fetch_url 을 사용해라. 내부 지식만으로 충분한 일반 대화에는 웹 도구를 쓰지 마라. 너는 도구 호출 루프 안에 있으며 필요하면 여러 번 도구를 호출할 수 있다.";
const TOOL_DEFINITIONS: OllamaToolDefinition[] = [
{
@@ -95,6 +96,48 @@ const TOOL_DEFINITIONS: OllamaToolDefinition[] = [
},
},
},
{
type: "function",
function: {
name: "web_search",
description: "웹 검색 결과 제목, URL, 요약을 가져온다. 최신 정보, 뉴스, 사실 확인이 필요할 때만 사용한다.",
parameters: {
type: "object",
required: ["query"],
properties: {
query: {
type: "string",
description: "검색어",
},
max_results: {
type: "number",
description: "가져올 최대 결과 수. 보통 3~5",
},
},
},
},
},
{
type: "function",
function: {
name: "fetch_url",
description: "주어진 URL의 페이지 제목과 본문 텍스트를 읽어온다. 검색 결과 상세 확인에 사용한다.",
parameters: {
type: "object",
required: ["url"],
properties: {
url: {
type: "string",
description: "http 또는 https URL",
},
max_chars: {
type: "number",
description: "본문에서 가져올 최대 글자 수",
},
},
},
},
},
];
export class OllamaLlmService {
@@ -159,7 +202,7 @@ export class OllamaLlmService {
}
for (const call of toolCalls) {
const result = this.executeTool(call);
const result = await this.executeTool(call);
this.logger.info("LLM tool call", {
name: call.function.name,
arguments: call.function.arguments,
@@ -213,7 +256,7 @@ export class OllamaLlmService {
};
}
private executeTool(call: OllamaToolCall): string {
private async executeTool(call: OllamaToolCall): Promise<string> {
switch (call.function.name) {
case "get_current_time":
return JSON.stringify(this.getCurrentTime());
@@ -226,6 +269,20 @@ export class OllamaLlmService {
expression: this.getStringArg(call.function.arguments, "expression"),
result: this.evaluateMath(this.getStringArg(call.function.arguments, "expression")),
});
case "web_search":
return JSON.stringify(
await webSearch(
this.getStringArg(call.function.arguments, "query"),
Math.min(5, Math.max(1, Math.trunc(this.getNumberArg(call.function.arguments, "max_results", 4)))),
),
);
case "fetch_url":
return JSON.stringify(
await webFetch(
this.getStringArg(call.function.arguments, "url"),
Math.min(10000, Math.max(1000, Math.trunc(this.getNumberArg(call.function.arguments, "max_chars", 6000)))),
),
);
default:
return JSON.stringify({
error: `unknown tool: ${call.function.name}`,
@@ -294,4 +351,18 @@ export class OllamaLlmService {
}
return result;
}
private getNumberArg(args: Record<string, unknown>, name: string, fallback: number): number {
const value = args[name];
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return fallback;
}
}