|
@@ -1,5 +1,6 @@
|
|
|
import type { LLMClient, LLMMessage, LLMResponse, OnStreamProgress } from "../llm/provider.js";
|
|
import type { LLMClient, LLMMessage, LLMResponse, OnStreamProgress } from "../llm/provider.js";
|
|
|
import { chatCompletion } from "../llm/provider.js";
|
|
import { chatCompletion } from "../llm/provider.js";
|
|
|
|
|
+import { searchWeb, fetchUrl } from "../utils/web-search.js";
|
|
|
import type { Logger } from "../utils/logger.js";
|
|
import type { Logger } from "../utils/logger.js";
|
|
|
|
|
|
|
|
export interface AgentContext {
|
|
export interface AgentContext {
|
|
@@ -18,6 +19,10 @@ export abstract class BaseAgent {
|
|
|
this.ctx = ctx;
|
|
this.ctx = ctx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ protected get log() {
|
|
|
|
|
+ return this.ctx.logger;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
protected async chat(
|
|
protected async chat(
|
|
|
messages: ReadonlyArray<LLMMessage>,
|
|
messages: ReadonlyArray<LLMMessage>,
|
|
|
options?: { readonly temperature?: number; readonly maxTokens?: number },
|
|
options?: { readonly temperature?: number; readonly maxTokens?: number },
|
|
@@ -29,20 +34,66 @@ export abstract class BaseAgent {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Chat with provider-native web search enabled.
|
|
|
|
|
- * OpenAI Chat API: uses web_search_options.
|
|
|
|
|
- * OpenAI Responses API: uses web_search_preview hosted tool.
|
|
|
|
|
- * Anthropic: falls back to regular chat (no native search).
|
|
|
|
|
|
|
+ * Chat with web search enabled.
|
|
|
|
|
+ * OpenAI: uses native web_search_options / web_search_preview.
|
|
|
|
|
+ * Other providers: searches via DuckDuckGo, fetches top results, injects into prompt.
|
|
|
*/
|
|
*/
|
|
|
protected async chatWithSearch(
|
|
protected async chatWithSearch(
|
|
|
messages: ReadonlyArray<LLMMessage>,
|
|
messages: ReadonlyArray<LLMMessage>,
|
|
|
options?: { readonly temperature?: number; readonly maxTokens?: number },
|
|
options?: { readonly temperature?: number; readonly maxTokens?: number },
|
|
|
): Promise<LLMResponse> {
|
|
): Promise<LLMResponse> {
|
|
|
- return chatCompletion(this.ctx.client, this.ctx.model, messages, {
|
|
|
|
|
- ...options,
|
|
|
|
|
- webSearch: true,
|
|
|
|
|
- onStreamProgress: this.ctx.onStreamProgress,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // OpenAI has native search — use it directly
|
|
|
|
|
+ if (this.ctx.client.provider === "openai") {
|
|
|
|
|
+ return chatCompletion(this.ctx.client, this.ctx.model, messages, {
|
|
|
|
|
+ ...options,
|
|
|
|
|
+ webSearch: true,
|
|
|
|
|
+ onStreamProgress: this.ctx.onStreamProgress,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Other providers: self-hosted search → inject results into prompt
|
|
|
|
|
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
|
|
|
+ if (!lastUserMsg) {
|
|
|
|
|
+ return this.chat(messages, options);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Extract search query from user message (first 200 chars)
|
|
|
|
|
+ const query = lastUserMsg.content.slice(0, 200);
|
|
|
|
|
+ this.log?.info(`[search] Searching: ${query.slice(0, 60)}...`);
|
|
|
|
|
+
|
|
|
|
|
+ const results = await searchWeb(query, 3);
|
|
|
|
|
+ if (results.length === 0) {
|
|
|
|
|
+ this.log?.warn("[search] No results found, falling back to regular chat");
|
|
|
|
|
+ return this.chat(messages, options);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Fetch top result for full content
|
|
|
|
|
+ let fullContent = "";
|
|
|
|
|
+ try {
|
|
|
|
|
+ fullContent = await fetchUrl(results[0]!.url, 4000);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // Fetch failed, use snippets only
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const searchContext = [
|
|
|
|
|
+ "## Web Search Results\n",
|
|
|
|
|
+ ...results.map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}`),
|
|
|
|
|
+ ...(fullContent ? [`\n## Full Content (Top Result)\n${fullContent}`] : []),
|
|
|
|
|
+ ].join("\n");
|
|
|
|
|
+
|
|
|
|
|
+ // Inject search results before the last user message
|
|
|
|
|
+ const augmentedMessages: LLMMessage[] = messages.map((m) =>
|
|
|
|
|
+ m === lastUserMsg
|
|
|
|
|
+ ? { ...m, content: `${searchContext}\n\n---\n\n${m.content}` }
|
|
|
|
|
+ : m,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return this.chat(augmentedMessages, options);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.log?.warn(`[search] Search failed: ${e}, falling back to regular chat`);
|
|
|
|
|
+ return this.chat(messages, options);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
abstract get name(): string;
|
|
abstract get name(): string;
|