diff --git a/.env b/.env new file mode 100644 index 0000000..e628578 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +# Replace the placeholder values with your actual API keys +OPENAI_API_KEY=your-openai-api-key +SERPAPI_API_KEY=your-serpapi-key \ No newline at end of file diff --git a/README.md b/README.md index 1526ce3..d21ccbb 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,51 @@ -# Deep Agent Search +# Deep Search Agent -This project demonstrates a simple search agent built with **LangChain**'s `DeepAgent` and the **OpenAI** language model. The agent can answer user queries and perform web searches when needed. +A minimal implementation of a deep search agent built from scratch using the LangChain framework and OpenAI API. +The agent decides whether to answer a query directly or perform a web search using SerpAPI. ## Prerequisites -- Node.js 18+ (ES modules support) -- An OpenAI API key. Set it in your environment: +- Node.js 18+ (ESM support) +- An OpenAI API key +- A SerpAPI key (free tier available) + +## Setup ```bash -export OPENAI_API_KEY="your-api-key-here" -``` +# Clone the repository +git clone https://github.com/your-username/deep-search-agent.git +cd deep-search-agent -## Installation - -```bash +# Install dependencies npm install + +# Create a .env file with your API keys +cp .env.example .env +# Edit .env and replace the placeholders with your actual keys ``` ## Usage -### CLI - -Run the agent interactively: +Run the agent with a query: ```bash -npm start +npm start -- "What is the capital of France?" ``` -You will be prompted to enter a question. The agent will respond. +The agent will output either a direct answer or the results of a web search. -### Programmatic +## How It Works -```js -import { ask } from "./src/index.js"; +1. **Planner** – Uses an LLM to decide if the query requires a web search or can be answered directly. +2. **Executor** – If a search is needed, the agent calls the SerpAPI tool and returns the results. +3. **Memory** – Stores conversation context (optional for future extensions). -async function main() { - const answer = await ask("Who wrote 'Pride and Prejudice'?"); - console.log(answer); -} +## Extending -main(); -``` - -## Testing - -A simple test script is provided: - -```bash -npm test -``` - -It queries the agent with a sample question and prints the answer. - -## Project Structure - -- `src/agent.js` – Configures the `DeepAgent` with OpenAI LLM and the search tool. -- `src/index.js` – Exposes the `ask` function and a CLI demo. -- `test.js` – Quick test script. -- `package.json` – Project metadata and dependencies. - -## Dependencies - -- `langchain` – Core LangChain library. -- `langchain-openai` – OpenAI wrapper for LangChain. -- `langchain-community` – Community tools, including the web search tool. +- Add more tools (e.g., Wikipedia, Calculator) and update the planner prompt accordingly. +- Replace the planner with a more sophisticated planner (e.g., chain of thought). +- Persist memory to a database for long‑term context. ## License -MIT \ No newline at end of file +MIT License \ No newline at end of file diff --git a/SOLUTION.md b/SOLUTION.md index 3e6d023..a3f3c40 100644 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -1,55 +1,63 @@ -**What was implemented** -- Added the required dependencies (`langchain-openai` and `langchain-community`) to `package.json`. -- Re‑implemented the search agent using LangChain’s `DeepAgent` instead of the previous custom logic. -- Configured the OpenAI LLM through the `langchain-openai` wrapper, reading the key from `OPENAI_API_KEY`. -- Integrated the built‑in `SearchTool` from `langchain-community` so the agent can perform web searches automatically. -- Exposed a simple `ask()` helper that invokes the agent and returns the output, and a CLI demo in `src/index.js`. +**Что реализовано** +- Добавлены пакеты `langchain-openai` и `langchain-community` в `package.json`. +- Создан класс `DeepAgent` в `src/index.js`, реализующий шаблон «Deep Agents from Scratch». +- Внутри агента реализован **планировщик** (`LLMChain` + `PromptTemplate`), который принимает запрос пользователя и возвращает JSON‑объект с типом действия (`search` или `answer`). +- В зависимости от плана агент либо вызывает инструмент `SerpAPI` для веб‑поиска, либо возвращает готовый ответ. +- Добавлена простая память (`BufferMemory`) для хранения истории диалога. +- В `main()` инициализируются LLM, инструмент поиска, память и агент, а затем агент обрабатывает запрос, переданный в командной строке. -**Why the main parts satisfy the requirements** -- **LangChain usage** – `DeepAgent` is instantiated directly (`src/agent.js`), meeting the “use LangChain’s Deep Agent API” constraint. -- **OpenAI API via langchain-openai** – The LLM is created with `new OpenAI({...})` from `langchain-openai`, ensuring all calls go through that package. -- **Dependencies added** – `langchain-openai` and `langchain-community` are listed in `package.json`, satisfying the dependency requirement. -- **No reliance on old code** – The previous custom agent logic is completely replaced; only the new LangChain components are used. -- **Search capability** – `SearchTool` is passed to the agent, allowing it to decide when to query the web, fulfilling the “search agent” goal. +**Почему это соответствует требованиям** +- **LangChain**: все взаимодействия с LLM и инструментами построены через `langchain`‑объекты (`OpenAI`, `SerpAPI`, `LLMChain`, `PromptTemplate`). +- **OpenAI API**: используется `OpenAI` из `langchain-openai` с ключом из переменной окружения `OPENAI_API_KEY`. +- **Deep Agent**: класс `DeepAgent` полностью соответствует шаблону «Deep Agents from Scratch» – отдельный планировщик, исполнитель и память. +- **Поиск**: при выборе `search` агент вызывает `SerpAPI.run(query)` и возвращает результат. +- **Ответ**: при выборе `answer` агент просто возвращает строку из плана. -**Key code excerpts** +**Ключевые фрагменты кода** `package.json` ```json "dependencies": { - "langchain": "^0.0.112", - "langchain-openai": "^0.0.112", - "langchain-community": "^0.0.112" + "langchain": "^0.0.0", + "langchain-openai": "^0.0.0", + "langchain-community": "^0.0.0", + "dotenv": "^16.0.0" } ``` -`src/agent.js` +`src/index.js` – планировщик ```js -import { DeepAgent } from "langchain/agents"; -import { OpenAI } from "langchain-openai"; -import { SearchTool } from "langchain-community/tools/search"; - -const llm = new OpenAI({ temperature: 0, modelName: "gpt-3.5-turbo" }); -const searchTool = new SearchTool(); - -const agent = new DeepAgent({ - llm, - tools: [searchTool], - verbose: true +this.planner = new LLMChain({ + llm: this.llm, + prompt: new PromptTemplate({ + inputVariables: ["input"], + template: `You are a helpful assistant. Given the user query: "{input}" +Decide whether you need to perform a web search or can answer directly. +Respond in JSON format: +{ + "type": "search" | "answer", + "query": "" | null, + "answer": "" | null +} +If you choose "search", provide the search query in "query". If you choose "answer", provide the answer in "answer".`, + }), }); ``` -`src/index.js` (invocation) +`src/index.js` – выполнение плана ```js -export async function ask(query) { - const result = await agent.invoke({ input: query }); - return result.output; +if (plan.type === "search" && plan.query) { + const searchTool = this.tools.find((t) => t.name === "SerpAPI"); + const searchResult = await searchTool.run(plan.query); + return searchResult; +} else if (plan.type === "answer" && plan.answer) { + return plan.answer; } ``` -**Honest limitations** -- The implementation assumes `OPENAI_API_KEY` is set; no fallback or user prompt is provided. -- No custom error handling beyond the basic try/catch in the CLI demo. -- The agent uses the default `SearchTool`; if a different search provider is needed, additional configuration would be required. +**Ограничения** +- Планировщик возвращает JSON, но не проверяет корректность ключей `type`, `query`, `answer` более глубоко. +- В случае ошибки в ответе LLM (невалидный JSON) агент выбрасывает исключение. +- Параметры модели и инструмента заданы статически; для гибкой конфигурации можно добавить CLI‑параметры. -Overall, the project now fully complies with the assignment: it uses LangChain, integrates OpenAI via the dedicated package, and rebuilds the search agent with the Deep Agent API. \ No newline at end of file +Таким образом, проект теперь содержит полноценного Deep Agent, использующего LangChain и OpenAI API, способного выполнять поисковые запросы и выдавать ответы. \ No newline at end of file diff --git a/package.json b/package.json index d5124ad..b6a2a34 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { - "name": "deep-agent-search", + "name": "deep-search-agent", "version": "1.0.0", - "description": "A simple search agent built with LangChain DeepAgent and OpenAI", + "description": "A deep search agent built from scratch using LangChain and OpenAI", "main": "src/index.js", "type": "module", "scripts": { - "start": "node src/index.js", - "test": "node test.js" + "start": "node src/index.js" }, "dependencies": { - "langchain": "^0.0.112", - "langchain-openai": "^0.0.112", - "langchain-community": "^0.0.112" + "langchain": "^0.0.0", + "langchain-openai": "^0.0.0", + "langchain-community": "^0.0.0", + "dotenv": "^16.0.0" } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index e111d46..fbb7237 100644 --- a/src/index.js +++ b/src/index.js @@ -1,34 +1,99 @@ -import agent from "./agent.js"; +import { OpenAI } from "langchain-openai"; +import { SerpAPI } from "langchain-community/tools/serpapi"; +import { PromptTemplate, LLMChain } from "langchain"; +import { BufferMemory } from "langchain/memory"; +import dotenv from "dotenv"; -/** - * Ask the agent a question and return the response. - * - * @param {string} query - The user query to send to the agent. - * @returns {Promise} - The agent's answer. - */ -export async function ask(query) { - const result = await agent.invoke({ input: query }); - return result.output; +dotenv.config(); + +class DeepAgent { + constructor(llm, tools, memory) { + this.llm = llm; + this.tools = tools; + this.memory = memory; + + // Planner: decides whether to search or answer directly + this.planner = new LLMChain({ + llm: this.llm, + prompt: new PromptTemplate({ + inputVariables: ["input"], + template: `You are a helpful assistant. Given the user query: "{input}" +Decide whether you need to perform a web search or can answer directly. +Respond in JSON format: +{ + "type": "search" | "answer", + "query": "" | null, + "answer": "" | null +} +If you choose "search", provide the search query in "query". If you choose "answer", provide the answer in "answer".`, + }), + }); + } + + async run(userInput) { + // Store user input in memory (optional) + await this.memory.saveContext({ input: userInput }, { output: "" }); + + // Planning step + const plannerOutput = await this.planner.call({ input: userInput }); + let plan; + try { + plan = JSON.parse(plannerOutput.output); + } catch (e) { + throw new Error("Planner output is not valid JSON"); + } + + // Execution step + if (plan.type === "search" && plan.query) { + const searchTool = this.tools.find((t) => t.name === "SerpAPI"); + if (!searchTool) { + throw new Error("Search tool not found"); + } + const searchResult = await searchTool.run(plan.query); + return searchResult; + } else if (plan.type === "answer" && plan.answer) { + return plan.answer; + } else { + throw new Error("Invalid plan produced by planner"); + } + } } -/** - * Simple CLI demo: read a query from stdin and print the agent's answer. - */ -if (import.meta.url === `file://${process.argv[1]}`) { - const readline = await import("readline"); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout +async function main() { + // Initialize LLM + const openai = new OpenAI({ + temperature: 0, + modelName: "gpt-3.5-turbo", + openAIApiKey: process.env.OPENAI_API_KEY, }); - rl.question("Enter your question: ", async (question) => { - try { - const answer = await ask(question); - console.log("\nAgent response:\n", answer); - } catch (err) { - console.error("Error:", err); - } finally { - rl.close(); - } + // Initialize search tool + const searchTool = new SerpAPI({ + apiKey: process.env.SERPAPI_API_KEY, + engine: "google", }); -} \ No newline at end of file + + // Memory (optional) + const memory = new BufferMemory({ memoryKey: "chat_history" }); + + // Create agent + const agent = new DeepAgent(openai, [searchTool], memory); + + // Get query from command line arguments + const query = process.argv.slice(2).join(" "); + if (!query) { + console.log("Please provide a query as a command line argument."); + process.exit(1); + } + + console.log(`Query: ${query}`); + try { + const result = await agent.run(query); + console.log("\nResult:"); + console.log(result); + } catch (err) { + console.error("Error:", err.message); + } +} + +main(); \ No newline at end of file