add: main.py — 8. Самописный поисковый агент на основе deep agents from scratch
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import os
|
||||
import asyncio
|
||||
import shutil
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import httpx
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langchain.tools import tool
|
||||
from deepagents import create_deep_agent
|
||||
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
|
||||
|
||||
# ---------- Configuration ----------
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
if not OPENAI_API_KEY:
|
||||
raise EnvironmentError("Please set the OPENAI_API_KEY environment variable.")
|
||||
|
||||
LLM = ChatOpenAI(
|
||||
model="openai/gpt-oss-20b:free",
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
api_key=OPENAI_API_KEY,
|
||||
temperature=0.0,
|
||||
)
|
||||
|
||||
# Workspace where the virtual filesystem lives
|
||||
WORKSPACE_DIR = Path("./workspace")
|
||||
OUTPUT_DIR = Path("./output")
|
||||
WORKSPACE_DIR.mkdir(exist_ok=True)
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
backend = CompositeBackend(
|
||||
[
|
||||
LocalShellBackend(workspace_dir=str(WORKSPACE_DIR)),
|
||||
FilesystemBackend(),
|
||||
]
|
||||
)
|
||||
|
||||
# ---------- Tools ----------
|
||||
@tool
|
||||
def web_search(query: str) -> str:
|
||||
"""
|
||||
Perform a simple web search using DuckDuckGo and return the titles and URLs of the top 3 results.
|
||||
The result is a JSON string with a list of objects: [{"title": "...", "url": "..."}].
|
||||
"""
|
||||
try:
|
||||
url = "https://html.duckduckgo.com/html/"
|
||||
params = {"q": query}
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
resp = httpx.get(url, params=params, headers=headers, timeout=15.0)
|
||||
resp.raise_for_status()
|
||||
soup = BeautifulSoup(resp.text, "html.parser")
|
||||
results = []
|
||||
for a in soup.select("a.result__a")[:3]:
|
||||
title = a.get_text(strip=True)
|
||||
link = a["href"]
|
||||
# DuckDuckGo wraps real URL in a redirect, extract the real one
|
||||
m = re.search(r"uddg=(.+)", link)
|
||||
if m:
|
||||
link = httpx.URL(m.group(1)).decode()
|
||||
results.append({"title": title, "url": link})
|
||||
return json.dumps(results, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error during web search: {e}"
|
||||
|
||||
@tool
|
||||
def write_virtual_file(path: str, content: str) -> str:
|
||||
"""
|
||||
Write a file into the virtual workspace. Path is relative to the workspace root.
|
||||
Returns a confirmation message.
|
||||
"""
|
||||
try:
|
||||
full_path = WORKSPACE_DIR / path
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
full_path.write_text(content, encoding="utf-8")
|
||||
return f"File written to {path}"
|
||||
except Exception as e:
|
||||
return f"Failed to write file: {e}"
|
||||
|
||||
@tool
|
||||
def list_virtual_files() -> str:
|
||||
"""
|
||||
List all files currently present in the virtual workspace.
|
||||
Returns a newline separated list of relative paths.
|
||||
"""
|
||||
try:
|
||||
files = [p.relative_to(WORKSPACE_DIR).as_posix() for p in WORKSPACE_DIR.rglob("*") if p.is_file()]
|
||||
return "\n".join(files) if files else "Workspace is empty."
|
||||
except Exception as e:
|
||||
return f"Error listing files: {e}"
|
||||
|
||||
@tool
|
||||
def export_workspace() -> str:
|
||||
"""
|
||||
Copy all files from the virtual workspace to the real output directory.
|
||||
Returns a summary of exported files.
|
||||
"""
|
||||
try:
|
||||
if OUTPUT_DIR.exists():
|
||||
shutil.rmtree(OUTPUT_DIR)
|
||||
shutil.copytree(WORKSPACE_DIR, OUTPUT_DIR)
|
||||
exported = [p.relative_to(OUTPUT_DIR).as_posix() for p in OUTPUT_DIR.rglob("*") if p.is_file()]
|
||||
return "Exported files:\\n" + "\\n".join(exported)
|
||||
except Exception as e:
|
||||
return f"Export failed: {e}"
|
||||
|
||||
# ---------- Agent ----------
|
||||
agent = create_deep_agent(
|
||||
model=LLM,
|
||||
tools=[web_search, write_virtual_file, list_virtual_files, export_workspace],
|
||||
backend=backend,
|
||||
system_prompt=(
|
||||
"You are a helpful research assistant. "
|
||||
"You can search the web, create virtual files, list them and finally export them to the real filesystem. "
|
||||
"When you have gathered enough information, write a summary to a file named 'report.txt' and then call export_workspace()."
|
||||
),
|
||||
)
|
||||
|
||||
# ---------- Main ----------
|
||||
async def main():
|
||||
user_query = (
|
||||
"Find the latest information about the James Webb Space Telescope discoveries, "
|
||||
"summarize the top three findings, and save the summary to a file called 'jws_summary.txt' in the workspace. "
|
||||
"After that, export all virtual files to the real output directory."
|
||||
)
|
||||
result = await agent.ainvoke(
|
||||
{"messages": [HumanMessage(content=user_query)]},
|
||||
{"configurable": {"thread_id": "search-session-1"}},
|
||||
)
|
||||
# Print the final assistant message
|
||||
final_message = result["messages"][-1].content
|
||||
print("=== Final Assistant Message ===")
|
||||
print(final_message)
|
||||
|
||||
# Show exported files
|
||||
if OUTPUT_DIR.exists():
|
||||
print("\\n=== Exported Files ===")
|
||||
for path in sorted(p.relative_to(OUTPUT_DIR).as_posix() for p in OUTPUT_DIR.rglob("*") if p.is_file()):
|
||||
print(path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user