// ---------------------------------------------------------------------------
// Tool: web_search
// Host-side web search MVP backed by the active model provider.
// ---------------------------------------------------------------------------
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { LlmAdapterService } from '../../../llm/index.js';
import { OpenAiCodexSseService, OpenAiOAuthFetchService, } from '../../../llm/adapters/openai/index.js';
import { ModelPolicyService } from '../../../llm/models/index.js';
import { DEFAULT_ANTHROPIC_MODEL, DEFAULT_OPENAI_MODEL } from '../../../config.js';
import { RuntimeCredentialService, } from '../../../runtime/credentials/index.js';
export const webSearchTool = createWebSearchTool();
export function createWebSearchTool(options = {}) {
    return {
        name: 'web_search',
        description: 'Search the public web when repository files, docs, and local tools are not enough to answer the user. This MVP tool is backed by the active model provider\'s hosted web search when available. Use it for current external facts, official product docs outside the repo, news, releases, APIs, or references that are not available locally. Do not use it for questions the workspace can already answer. Input example: { "query": "OpenAI Responses API web search tool" }. Optional field: contextSize ("low", "medium", or "high"). Returns a concise summary plus cited source URLs when available.',
        parameters: {
            type: 'object',
            additionalProperties: false,
            properties: {
                query: {
                    type: 'string',
                    description: 'The web search query.',
                },
                contextSize: {
                    type: 'string',
                    enum: ['low', 'medium', 'high'],
                    description: 'How much search context to request from the hosted search tool. Defaults to "medium".',
                },
            },
            required: ['query'],
        },
        async execute(raw) {
            if (!isWebSearchInput(raw)) {
                return {
                    ok: false,
                    error: 'Invalid input for web_search. Required field: query. Optional field: contextSize ("low", "medium", or "high").',
                };
            }
            const input = raw;
            const provider = options.provider ?? LlmAdapterService.inferProvider(options.model ?? DEFAULT_OPENAI_MODEL);
            try {
                switch (provider) {
                    case 'openai':
                        return await executeOpenAiWebSearch(input, options);
                    case 'anthropic':
                        return await executeAnthropicWebSearch(input, options);
                    case 'google':
                        return {
                            ok: false,
                            error: 'web_search is not wired for Google models yet.',
                        };
                    case 'ollama':
                    case 'huggingface':
                        return {
                            ok: false,
                            error: `web_search is not wired for ${provider} models yet.`,
                        };
                }
            }
            catch (error) {
                return {
                    ok: false,
                    error: `Web search failed: ${error instanceof Error ? error.message : String(error)}`,
                };
            }
        },
    };
}
async function executeOpenAiWebSearch(input, options) {
    const model = options.model ?? process.env.OPENAI_WEB_SEARCH_MODEL ?? DEFAULT_OPENAI_MODEL;
    const oauthCredential = options.providerCredentialSource?.type === 'oauth' ?
        RuntimeCredentialService.resolveOAuthCredentialForModel(model, { storePath: options.credentialStorePath })
        : undefined;
    if (options.providerCredentialSource?.type === 'oauth' && !oauthCredential) {
        return {
            ok: false,
            error: 'web_search could not load the stored OpenAI account sign-in credential for this workspace. Sign in again with `heddle auth login openai`, or set OPENAI_API_KEY to use Platform API-key mode.',
        };
    }
    const compatibility = ModelPolicyService.validateCredentialCompatibility({
        model,
        provider: 'openai',
        credentialMode: oauthCredential ? 'oauth' : undefined,
        usageLabel: 'web search',
    });
    if (!compatibility.ok) {
        return {
            ok: false,
            error: compatibility.error,
        };
    }
    if (oauthCredential) {
        return await executeOpenAiOAuthWebSearch(input, { ...options, model }, oauthCredential);
    }
    const apiKey = firstDefinedNonEmpty(options.apiKey, process.env.OPENAI_API_KEY, process.env.PERSONAL_OPENAI_API_KEY);
    if (!apiKey) {
        return {
            ok: false,
            error: 'web_search requires OPENAI_API_KEY (or PERSONAL_OPENAI_API_KEY) when the active model provider is OpenAI.',
        };
    }
    const client = new OpenAI({ apiKey });
    const response = await client.responses.create({
        model,
        input: input.query,
        tools: [{
                type: 'web_search_preview',
                search_context_size: input.contextSize ?? 'medium',
            }],
    });
    return {
        ok: true,
        output: formatOpenAiWebSearchResult(response),
    };
}
async function executeOpenAiOAuthWebSearch(input, options, oauthCredential) {
    const oauthFetch = OpenAiOAuthFetchService.create(oauthCredential, { storePath: options.credentialStorePath });
    const sseText = await OpenAiCodexSseService.execute({
        oauthFetch,
        body: {
            model: options.model,
            store: false,
            stream: true,
            reasoning: { summary: 'auto' },
            instructions: 'Search the web and answer concisely with citations when available.',
            include: ['web_search_call.action.sources'],
            input: [{ type: 'message', role: 'user', content: input.query }],
            tools: [{
                    type: 'web_search',
                    search_context_size: input.contextSize ?? 'medium',
                }],
        },
    });
    return {
        ok: true,
        output: formatOpenAiOAuthWebSearchSseResult(sseText, options.model),
    };
}
async function executeAnthropicWebSearch(input, options) {
    const apiKey = firstDefinedNonEmpty(options.apiKey, process.env.ANTHROPIC_API_KEY, process.env.PERSONAL_ANTHROPIC_API_KEY);
    if (!apiKey) {
        return {
            ok: false,
            error: 'web_search requires ANTHROPIC_API_KEY (or PERSONAL_ANTHROPIC_API_KEY) when the active model provider is Anthropic.',
        };
    }
    const client = new Anthropic({ apiKey });
    const model = options.model ?? process.env.ANTHROPIC_WEB_SEARCH_MODEL ?? DEFAULT_ANTHROPIC_MODEL;
    const toolChoice = { type: 'tool', name: 'web_search' };
    const response = await client.messages.create({
        model,
        max_tokens: 1024,
        tool_choice: toolChoice,
        tools: [{
                name: 'web_search',
                type: 'web_search_20250305',
                max_uses: 1,
            }],
        messages: [{
                role: 'user',
                content: `Search the web for the following query and answer concisely with citations when available:\n\n${input.query}`,
            }],
    });
    return {
        ok: true,
        output: formatAnthropicWebSearchResult(response),
    };
}
function firstDefinedNonEmpty(...values) {
    return values.find((value) => typeof value === 'string' && value.trim().length > 0);
}
function isWebSearchInput(raw) {
    if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
        return false;
    }
    const input = raw;
    const keys = Object.keys(input);
    if (keys.some((key) => key !== 'query' && key !== 'contextSize')) {
        return false;
    }
    if (typeof input.query !== 'string' || input.query.trim().length === 0) {
        return false;
    }
    return input.contextSize === undefined || input.contextSize === 'low' || input.contextSize === 'medium' || input.contextSize === 'high';
}
function formatOpenAiWebSearchResult(response) {
    const summary = response.output_text?.trim() || 'No summary returned.';
    const citations = extractOpenAiUrlCitations(response);
    return {
        provider: 'openai',
        model: response.model,
        summary,
        citations,
    };
}
function extractOpenAiUrlCitations(response) {
    const outputItems = response.output ?? [];
    const citations = [];
    const seen = new Set();
    for (const item of outputItems) {
        if (item.type !== 'message') {
            continue;
        }
        for (const content of item.content ?? []) {
            if (content.type !== 'output_text') {
                continue;
            }
            for (const annotation of content.annotations ?? []) {
                if (annotation.type !== 'url_citation') {
                    continue;
                }
                const key = `${annotation.title}|${annotation.url}`;
                if (seen.has(key)) {
                    continue;
                }
                seen.add(key);
                citations.push({
                    title: annotation.title,
                    url: annotation.url,
                });
            }
        }
    }
    return citations;
}
function formatOpenAiOAuthWebSearchSseResult(sseText, model) {
    return {
        provider: 'openai',
        model,
        summary: OpenAiCodexSseService.extractOutputText(sseText).trim() || 'No summary returned.',
        citations: extractSseWebSearchSources(sseText),
    };
}
function extractSseWebSearchSources(sseText) {
    const citations = [];
    const seen = new Set();
    const items = OpenAiCodexSseService.extractItems(sseText, (item) => Boolean(item && typeof item === 'object' && item.type === 'web_search_call'));
    for (const item of items) {
        for (const source of item.action?.sources ?? []) {
            if (source.type !== 'url' || !source.url) {
                continue;
            }
            if (seen.has(source.url)) {
                continue;
            }
            seen.add(source.url);
            citations.push({
                title: source.url,
                url: source.url,
            });
        }
    }
    return citations;
}
function formatAnthropicWebSearchResult(response) {
    const textBlocks = response.content.filter((block) => block.type === 'text');
    const summary = textBlocks.map((block) => block.text).join('\n').trim() || 'No summary returned.';
    const citations = extractAnthropicUrlCitations(textBlocks.flatMap((block) => block.citations ?? []));
    return {
        provider: 'anthropic',
        model: response.model,
        summary,
        citations,
    };
}
function extractAnthropicUrlCitations(citationsInput) {
    const citations = [];
    const seen = new Set();
    for (const citation of citationsInput) {
        if (citation.type !== 'web_search_result_location') {
            continue;
        }
        if (!citation.url) {
            continue;
        }
        const title = citation.title ?? citation.url;
        const key = `${title}|${citation.url}`;
        if (seen.has(key)) {
            continue;
        }
        seen.add(key);
        citations.push({
            title,
            url: citation.url,
        });
    }
    return citations;
}
//# sourceMappingURL=web-search.js.map