add: main.py — 8. Самописный поисковый агент на основе deep agents from scratch

This commit is contained in:
2026-06-30 17:39:12 +00:00
commit 1f19bab54b
+146
View File
@@ -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())