feature/pi-refactor
  1/**
  2 * Research Tool - Search the web and synthesize results
  3 * 
  4 * This tool combines web search with LLM synthesis to provide
  5 * comprehensive research answers with sources.
  6 */
  7
  8import { Type } from "@sinclair/typebox";
  9import type { AgentTool } from "@mariozechner/pi-agent-core";
 10import { webSearchTool } from "./websearch.js";
 11import { getDefaultModel } from "../config.js";
 12import { completeSimple } from "@mariozechner/pi-ai";
 13
 14export const researchTool: AgentTool = {
 15  name: "research",
 16  label: "Research",
 17  description: "Research a topic by searching the web and synthesizing the results into a comprehensive answer with sources.",
 18  parameters: Type.Object({
 19    topic: Type.String({ description: "The research topic or question" }),
 20  }),
 21  execute: async (_toolCallId, params, signal, _onUpdate) => {
 22    const { topic } = params as { topic: string };
 23
 24    if (!topic || topic.trim().length === 0) {
 25      return {
 26        content: [
 27          {
 28            type: "text" as const,
 29            text: "Error: Research topic cannot be empty.",
 30          },
 31        ],
 32        details: { error: "empty_topic" },
 33      };
 34    }
 35
 36    try {
 37      // Step 1: Search the web
 38      const searchResult = await webSearchTool.execute(
 39        "research-search",
 40        { query: topic, maxResults: 5 },
 41        signal || new AbortController().signal,
 42        () => {}
 43      );
 44
 45      // Extract search results text
 46      const searchText = searchResult.content
 47        .filter((c: any) => c.type === "text")
 48        .map((c: any) => c.text)
 49        .join("\n\n");
 50
 51      // Check if we got any results
 52      if (searchText.includes("No results found")) {
 53        return {
 54          content: [
 55            {
 56              type: "text" as const,
 57              text: `I couldn't find relevant web results for "${topic}". The topic might be too specific, or there might be connectivity issues. Please try rephrasing your question or being more general.`,
 58            },
 59          ],
 60          details: {
 61            topic,
 62            searchResults: 0,
 63          },
 64        };
 65      }
 66
 67      // Step 2: Synthesize with LLM
 68      const model = getDefaultModel();
 69      
 70      const synthesisPrompt = `You are a research assistant. Based on the following web search results, provide a comprehensive and well-structured answer about: "${topic}"
 71
 72Search Results:
 73${searchText}
 74
 75Please:
 761. Synthesize the information into a clear, comprehensive answer
 772. Include the most important facts and details
 783. Cite sources by mentioning the URLs where relevant
 794. Structure your answer with clear sections if appropriate
 805. Be objective and factual
 81
 82Provide your research summary:`;
 83
 84      const synthesis = await completeSimple(model, {
 85        messages: [
 86          {
 87            role: "user",
 88            content: synthesisPrompt,
 89            timestamp: Date.now(),
 90          },
 91        ],
 92      });
 93
 94      const synthesizedText = typeof synthesis.content === "string" 
 95        ? synthesis.content 
 96        : synthesis.content
 97            .filter((c: any) => c.type === "text")
 98            .map((c: any) => c.text)
 99            .join("\n");
100
101      if (!synthesizedText.trim()) {
102        return {
103          content: [
104            {
105              type: "text" as const,
106              text: `Research completed for "${topic}", but synthesis produced no output. Raw search results:\n\n${searchText}`,
107            },
108          ],
109          details: {
110            topic,
111            searchResults: searchResult.details?.resultCount || 0,
112            synthesisError: "empty_synthesis",
113          },
114        };
115      }
116
117      // Return synthesized research
118      return {
119        content: [
120          {
121            type: "text" as const,
122            text: synthesizedText.trim(),
123          },
124        ],
125        details: {
126          topic,
127          searchResults: searchResult.details?.resultCount || 0,
128          synthesized: true,
129        },
130      };
131    } catch (error) {
132      console.error("Research error:", error);
133      
134      return {
135        content: [
136          {
137            type: "text" as const,
138            text: `Error conducting research on "${topic}": ${error instanceof Error ? error.message : "Unknown error"}`,
139          },
140        ],
141        details: {
142          topic,
143          error: error instanceof Error ? error.message : "unknown_error",
144        },
145      };
146    }
147  },
148};