feat: Intelligence Layer Phase 1 — orchestrator service
Adds the Gemini API orchestrator (ReAct tool loop → Claude responder):
Orchestrator engine + router:
- orchestrator_engine.py: Gemini API tool loop, Claude CLI handoff
- routers/orchestrator.py: POST /orchestrate (async job queue), GET /orchestrate/{job_id}
Tools (cortex/tools/):
- web.py: DuckDuckGo web search (no key required)
- ae_knowledge.py: ae_journal_search + ae_journal_entry_create (AE V3 API)
- ae_tasks.py: ae_task_list (reads agents_sync Kanban filesystem)
- files.py: file_read (path-allowlisted to safe dirs)
Config + deps:
- config.py: orchestrator, DuckDuckGo, and AE API settings
- requirements.txt: google-genai, duckduckgo-search
- .env.default: reference config with all new keys documented
Docs:
- CLAUDE.md, README.md, documentation/ added to repo
- Port references updated 7331 → 8000 throughout
- Default model updated to gemini-2.5-flash
Tested: ae_task_list, ae_journal_search, web_search all working end-to-end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
50
cortex/tools/web.py
Normal file
50
cortex/tools/web.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Web search tool — DuckDuckGo backend.
|
||||
|
||||
Uses the duckduckgo-search library. Set DDG_API_KEY in .env for a paid account
|
||||
(higher rate limits). The free unauthenticated tier works for moderate usage.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def search(query: str, max_results: int | None = None) -> str:
|
||||
"""Search DuckDuckGo and return results as a formatted string.
|
||||
|
||||
Returns a markdown-formatted list of results: title, URL, and snippet.
|
||||
The orchestrator includes this in the context it passes to Claude.
|
||||
"""
|
||||
n = min(max_results or settings.ddg_max_results, 10)
|
||||
results = await asyncio.to_thread(_sync_search, query, n)
|
||||
if not results:
|
||||
return f"No results found for: {query}"
|
||||
|
||||
lines = [f"Search results for: **{query}**\n"]
|
||||
for i, r in enumerate(results, 1):
|
||||
lines.append(f"{i}. [{r['title']}]({r['href']})")
|
||||
if r.get("body"):
|
||||
lines.append(f" {r['body']}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def _sync_search(query: str, max_results: int) -> list[dict]:
|
||||
"""Synchronous DuckDuckGo search — run via asyncio.to_thread."""
|
||||
from duckduckgo_search import DDGS
|
||||
|
||||
kwargs = {}
|
||||
if settings.ddg_api_key:
|
||||
# Paid account — pass token for higher rate limits
|
||||
kwargs["headers"] = {"Authorization": f"Bearer {settings.ddg_api_key}"}
|
||||
|
||||
try:
|
||||
with DDGS(**kwargs) as ddgs:
|
||||
return list(ddgs.text(query, max_results=max_results))
|
||||
except Exception as e:
|
||||
logger.warning("DuckDuckGo search error: %s", e)
|
||||
return []
|
||||
Reference in New Issue
Block a user