Spaces:
Sleeping
Sleeping
Cyber Catalyst Team commited on
Commit ·
6733a58
1
Parent(s): 2e782b6
Implement workspace isolation, active MCP tools, and Jina search/read tools
Browse files- backend.py +342 -31
backend.py
CHANGED
|
@@ -27,6 +27,10 @@ import collections
|
|
| 27 |
from pathlib import Path
|
| 28 |
from typing import AsyncIterator, Optional, List, Dict, Any
|
| 29 |
from pydantic import BaseModel
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# --- Ultimate Agent Brain Imports ---
|
| 32 |
from second_brain import SecondBrainWrapper
|
|
@@ -36,7 +40,8 @@ from helix_state import helix_db
|
|
| 36 |
from context_engine import ContextEngine
|
| 37 |
|
| 38 |
# Instantiate singletons for the orchestrator
|
| 39 |
-
|
|
|
|
| 40 |
context_engine = ContextEngine(brain)
|
| 41 |
watchdog = SurvivalWatchdog()
|
| 42 |
|
|
@@ -244,6 +249,40 @@ TOOLS = [
|
|
| 244 |
}
|
| 245 |
}
|
| 246 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
]
|
| 248 |
|
| 249 |
# ---------------------------------------------------------------------------
|
|
@@ -252,7 +291,7 @@ TOOLS = [
|
|
| 252 |
|
| 253 |
def _safe_path(rel_path: str) -> Path:
|
| 254 |
"""Resolve a relative path safely within the workspace."""
|
| 255 |
-
workspace = Path(
|
| 256 |
target = (workspace / rel_path).resolve()
|
| 257 |
# Prevent path traversal
|
| 258 |
if not str(target).startswith(str(workspace)):
|
|
@@ -301,7 +340,174 @@ def repair_arguments(func_name: str, args: dict) -> tuple[dict, list[str]]:
|
|
| 301 |
return repaired_args, notes
|
| 302 |
|
| 303 |
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
"""Execute a tool and return its output as a string asynchronously."""
|
| 306 |
try:
|
| 307 |
if name == "read_file":
|
|
@@ -333,7 +539,7 @@ async def execute_tool(name: str, arguments: dict) -> str:
|
|
| 333 |
command,
|
| 334 |
stdout=asyncio.subprocess.PIPE,
|
| 335 |
stderr=asyncio.subprocess.PIPE,
|
| 336 |
-
cwd=
|
| 337 |
env={**os.environ, "HOME": "/tmp", "PATH": os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin")},
|
| 338 |
)
|
| 339 |
|
|
@@ -390,7 +596,7 @@ async def execute_tool(name: str, arguments: dict) -> str:
|
|
| 390 |
"grep", "-rn", "--include=*", pattern, str(path),
|
| 391 |
stdout=asyncio.subprocess.PIPE,
|
| 392 |
stderr=asyncio.subprocess.PIPE,
|
| 393 |
-
cwd=
|
| 394 |
)
|
| 395 |
try:
|
| 396 |
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=10)
|
|
@@ -407,6 +613,40 @@ async def execute_tool(name: str, arguments: dict) -> str:
|
|
| 407 |
output = output[:10000] + "\n\n[Truncated]"
|
| 408 |
return output
|
| 409 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
else:
|
| 411 |
return f"Error: Unknown tool: {name}"
|
| 412 |
|
|
@@ -708,12 +948,19 @@ async def chat_completions(request: Request, authorization: str = Header(None)):
|
|
| 708 |
if user_msg:
|
| 709 |
await save_message(session_id, "user", user_msg.get("content", ""))
|
| 710 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
if not stream:
|
| 712 |
# Non-streaming: simple completion
|
|
|
|
| 713 |
try:
|
|
|
|
|
|
|
| 714 |
kwargs = {"model": requested_model, "messages": final_messages}
|
| 715 |
if is_agentic:
|
| 716 |
-
kwargs["tools"] =
|
| 717 |
kwargs["tool_choice"] = "auto"
|
| 718 |
async with completions_semaphore:
|
| 719 |
response = await client.chat.completions.create(**kwargs)
|
|
@@ -731,18 +978,23 @@ async def chat_completions(request: Request, authorization: str = Header(None)):
|
|
| 731 |
except Exception as e:
|
| 732 |
ACTIVE_SESSIONS.discard(session_id)
|
| 733 |
return JSONResponse({"error": {"message": str(e), "type": "internal_error"}}, status_code=500)
|
|
|
|
|
|
|
| 734 |
|
| 735 |
# Streaming + agentic loop
|
| 736 |
async def generate() -> AsyncIterator[str]:
|
| 737 |
nonlocal final_messages
|
| 738 |
-
|
| 739 |
-
|
|
|
|
|
|
|
|
|
|
| 740 |
for round_num in range(MAX_TOOL_ROUNDS + 1):
|
| 741 |
# Perform auto-compaction before calling NIM API
|
| 742 |
final_messages = compact_history(final_messages)
|
| 743 |
kwargs = {"model": requested_model, "messages": final_messages, "stream": True}
|
| 744 |
if is_agentic:
|
| 745 |
-
kwargs["tools"] =
|
| 746 |
kwargs["tool_choice"] = "auto"
|
| 747 |
|
| 748 |
# Collect streamed response
|
|
@@ -836,7 +1088,6 @@ async def chat_completions(request: Request, authorization: str = Header(None)):
|
|
| 836 |
yield make_chunk(request_id, requested_model, f"\n\n🔧 **{func_name}**")
|
| 837 |
if repair_notes:
|
| 838 |
yield make_chunk(request_id, requested_model, " *(Auto-Repaired)*")
|
| 839 |
-
|
| 840 |
if func_name == "run_bash" and "command" in repaired_args:
|
| 841 |
yield make_chunk(request_id, requested_model, f": `{repaired_args['command']}`\n")
|
| 842 |
elif func_name == "read_file" and "path" in repaired_args:
|
|
@@ -847,11 +1098,17 @@ async def chat_completions(request: Request, authorization: str = Header(None)):
|
|
| 847 |
yield make_chunk(request_id, requested_model, f": `{repaired_args.get('path', '.')}`\n")
|
| 848 |
elif func_name == "grep_search":
|
| 849 |
yield make_chunk(request_id, requested_model, f": `{repaired_args.get('pattern', '')}`\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
else:
|
| 851 |
yield make_chunk(request_id, requested_model, "\n")
|
| 852 |
|
| 853 |
# Execute the tool
|
| 854 |
-
result = await execute_tool(func_name, repaired_args)
|
| 855 |
|
| 856 |
# Append teaching note if repaired
|
| 857 |
if repair_notes:
|
|
@@ -877,11 +1134,13 @@ async def chat_completions(request: Request, authorization: str = Header(None)):
|
|
| 877 |
yield make_chunk(request_id, requested_model, finish_reason="stop")
|
| 878 |
yield "data: [DONE]\n\n"
|
| 879 |
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
|
|
|
|
|
|
| 885 |
|
| 886 |
return StreamingResponse(
|
| 887 |
generate(),
|
|
@@ -954,7 +1213,18 @@ async def get_workspace_tree():
|
|
| 954 |
}
|
| 955 |
|
| 956 |
try:
|
| 957 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 958 |
if not w_path.exists():
|
| 959 |
w_path.mkdir(parents=True, exist_ok=True)
|
| 960 |
return build_tree(w_path, w_path)
|
|
@@ -965,11 +1235,27 @@ async def get_workspace_tree():
|
|
| 965 |
@app.get("/api/workspace/file")
|
| 966 |
async def get_workspace_file(path: str):
|
| 967 |
try:
|
| 968 |
-
|
| 969 |
-
if
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
except Exception as e:
|
| 974 |
raise HTTPException(status_code=500, detail=str(e))
|
| 975 |
|
|
@@ -977,7 +1263,18 @@ async def get_workspace_file(path: str):
|
|
| 977 |
@app.get("/api/workspace/latest-screenshot")
|
| 978 |
async def get_latest_screenshot():
|
| 979 |
try:
|
| 980 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
png_files = []
|
| 982 |
for p in w_path.rglob("*.png"):
|
| 983 |
if any(part.startswith(".") for part in p.parts):
|
|
@@ -1747,7 +2044,21 @@ class ForgeExecuteRequest(BaseModel):
|
|
| 1747 |
async def forge_execute(req: ForgeExecuteRequest, authorization: str = Header(None)):
|
| 1748 |
auth(authorization)
|
| 1749 |
log_activity(f"[Forge] Received execution request for task {req.task_id}: '{req.prompt[:80]}'")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1750 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1751 |
# Inject project_state.md into system context if provided (Karpathy protocol)
|
| 1752 |
system_content = AGENTIC_SYSTEM_PROMPT + f"\nContext rules: {req.context_rules}"
|
| 1753 |
if req.project_state_md:
|
|
@@ -1770,7 +2081,7 @@ async def forge_execute(req: ForgeExecuteRequest, authorization: str = Header(No
|
|
| 1770 |
response = await nim_client.chat.completions.create(
|
| 1771 |
model=RECOMMENDED_MODEL,
|
| 1772 |
messages=final_messages,
|
| 1773 |
-
tools=
|
| 1774 |
tool_choice="auto",
|
| 1775 |
timeout=8.0
|
| 1776 |
)
|
|
@@ -1781,7 +2092,7 @@ async def forge_execute(req: ForgeExecuteRequest, authorization: str = Header(No
|
|
| 1781 |
response = await mistral_client.chat.completions.create(
|
| 1782 |
model="mistral-large-latest",
|
| 1783 |
messages=final_messages,
|
| 1784 |
-
tools=
|
| 1785 |
tool_choice="auto"
|
| 1786 |
)
|
| 1787 |
else:
|
|
@@ -1831,7 +2142,7 @@ async def forge_execute(req: ForgeExecuteRequest, authorization: str = Header(No
|
|
| 1831 |
func_args = json.loads(repaired_str)
|
| 1832 |
except Exception:
|
| 1833 |
func_args = {}
|
| 1834 |
-
result = await execute_tool(func_name, func_args)
|
| 1835 |
|
| 1836 |
final_messages.append({
|
| 1837 |
"role": "tool",
|
|
@@ -1859,11 +2170,8 @@ async def forge_execute(req: ForgeExecuteRequest, authorization: str = Header(No
|
|
| 1859 |
"status": "error",
|
| 1860 |
"error": str(e)
|
| 1861 |
}
|
| 1862 |
-
|
| 1863 |
-
|
| 1864 |
-
# ---------------------------------------------------------------------------
|
| 1865 |
-
# Database Retention & Heartbeat loops
|
| 1866 |
-
# ---------------------------------------------------------------------------
|
| 1867 |
|
| 1868 |
async def db_heartbeat_loop():
|
| 1869 |
log_activity("Database Heartbeat task started")
|
|
@@ -2513,6 +2821,9 @@ async def db_heartbeat_loop():
|
|
| 2513 |
|
| 2514 |
|
| 2515 |
def run_backup_loop():
|
|
|
|
|
|
|
|
|
|
| 2516 |
log_activity("Local Git Backup Loop started")
|
| 2517 |
while True:
|
| 2518 |
# Wait 5 minutes between runs
|
|
|
|
| 27 |
from pathlib import Path
|
| 28 |
from typing import AsyncIterator, Optional, List, Dict, Any
|
| 29 |
from pydantic import BaseModel
|
| 30 |
+
import contextvars
|
| 31 |
+
import urllib.parse
|
| 32 |
+
|
| 33 |
+
workspace_var = contextvars.ContextVar("workspace_dir", default="/tmp/workspace")
|
| 34 |
|
| 35 |
# --- Ultimate Agent Brain Imports ---
|
| 36 |
from second_brain import SecondBrainWrapper
|
|
|
|
| 40 |
from context_engine import ContextEngine
|
| 41 |
|
| 42 |
# Instantiate singletons for the orchestrator
|
| 43 |
+
SPACE_NAME = os.environ.get("SPACE_NAME", "space2-cerebrum")
|
| 44 |
+
brain = SecondBrainWrapper(space_name=SPACE_NAME)
|
| 45 |
context_engine = ContextEngine(brain)
|
| 46 |
watchdog = SurvivalWatchdog()
|
| 47 |
|
|
|
|
| 249 |
}
|
| 250 |
}
|
| 251 |
},
|
| 252 |
+
{
|
| 253 |
+
"type": "function",
|
| 254 |
+
"function": {
|
| 255 |
+
"name": "web_search",
|
| 256 |
+
"description": "Search the web for up-to-date information, news, papers, or documentation.",
|
| 257 |
+
"parameters": {
|
| 258 |
+
"type": "object",
|
| 259 |
+
"properties": {
|
| 260 |
+
"query": {
|
| 261 |
+
"type": "string",
|
| 262 |
+
"description": "The search query (be specific and detailed)"
|
| 263 |
+
}
|
| 264 |
+
},
|
| 265 |
+
"required": ["query"]
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"type": "function",
|
| 271 |
+
"function": {
|
| 272 |
+
"name": "web_read",
|
| 273 |
+
"description": "Read the clean markdown content of any webpage URL to get detailed context, articles, documentation, or code.",
|
| 274 |
+
"parameters": {
|
| 275 |
+
"type": "object",
|
| 276 |
+
"properties": {
|
| 277 |
+
"url": {
|
| 278 |
+
"type": "string",
|
| 279 |
+
"description": "The absolute URL of the webpage to read"
|
| 280 |
+
}
|
| 281 |
+
},
|
| 282 |
+
"required": ["url"]
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
},
|
| 286 |
]
|
| 287 |
|
| 288 |
# ---------------------------------------------------------------------------
|
|
|
|
| 291 |
|
| 292 |
def _safe_path(rel_path: str) -> Path:
|
| 293 |
"""Resolve a relative path safely within the workspace."""
|
| 294 |
+
workspace = Path(workspace_var.get()).resolve()
|
| 295 |
target = (workspace / rel_path).resolve()
|
| 296 |
# Prevent path traversal
|
| 297 |
if not str(target).startswith(str(workspace)):
|
|
|
|
| 340 |
return repaired_args, notes
|
| 341 |
|
| 342 |
|
| 343 |
+
# ---------------------------------------------------------------------------
|
| 344 |
+
# Search and MCP Helpers
|
| 345 |
+
# ---------------------------------------------------------------------------
|
| 346 |
+
|
| 347 |
+
def sanitize_function_name(name: str) -> str:
|
| 348 |
+
sanitized = re.sub(r'[^a-zA-Z0-9_\.\-]', '_', name)
|
| 349 |
+
if not re.match(r'^[a-zA-Z_]', sanitized):
|
| 350 |
+
sanitized = '_' + sanitized
|
| 351 |
+
if len(sanitized) > 124:
|
| 352 |
+
sanitized = sanitized[:124]
|
| 353 |
+
return sanitized
|
| 354 |
+
|
| 355 |
+
def create_mcp_tool_id(server_name: str, tool_name: str) -> str:
|
| 356 |
+
san_server = sanitize_function_name(server_name)
|
| 357 |
+
san_tool = sanitize_function_name(tool_name)
|
| 358 |
+
max_len = 124
|
| 359 |
+
sep = "_"
|
| 360 |
+
if len(san_server) + len(san_tool) + len(sep) > max_len:
|
| 361 |
+
total = len(san_server) + len(san_tool)
|
| 362 |
+
server_portion = int((len(san_server) / total) * (max_len - len(sep)))
|
| 363 |
+
tool_portion = max_len - len(sep) - server_portion
|
| 364 |
+
return f"{san_server[:server_portion]}{sep}{san_tool[:tool_portion]}"
|
| 365 |
+
return f"{san_server}{sep}{san_tool}"
|
| 366 |
+
|
| 367 |
+
async def get_active_mcp_tools():
|
| 368 |
+
"""
|
| 369 |
+
Queries `mcp_server` table and returns:
|
| 370 |
+
1. List of OpenAI function definitions to append to dynamic tools list.
|
| 371 |
+
2. Dictionary mapping `mcp_tool_id` to `(server_name, original_tool_name, config)`.
|
| 372 |
+
"""
|
| 373 |
+
mcp_tools_list = []
|
| 374 |
+
mcp_mapping = {}
|
| 375 |
+
if not db_pool:
|
| 376 |
+
return mcp_tools_list, mcp_mapping
|
| 377 |
+
try:
|
| 378 |
+
async with db_pool.acquire() as conn:
|
| 379 |
+
rows = await conn.fetch("SELECT name, config, tool_info FROM mcp_server WHERE enabled = true")
|
| 380 |
+
for row in rows:
|
| 381 |
+
server_name = row["name"]
|
| 382 |
+
config_raw = row["config"]
|
| 383 |
+
if isinstance(config_raw, str):
|
| 384 |
+
config = json.loads(config_raw)
|
| 385 |
+
else:
|
| 386 |
+
config = config_raw
|
| 387 |
+
tool_info_raw = row["tool_info"]
|
| 388 |
+
if not tool_info_raw:
|
| 389 |
+
continue
|
| 390 |
+
if isinstance(tool_info_raw, str):
|
| 391 |
+
tool_info = json.loads(tool_info_raw)
|
| 392 |
+
else:
|
| 393 |
+
tool_info = tool_info_raw
|
| 394 |
+
for tool in tool_info:
|
| 395 |
+
tool_name = tool.get("name")
|
| 396 |
+
description = tool.get("description", "")
|
| 397 |
+
input_schema = tool.get("inputSchema", {})
|
| 398 |
+
tool_id = create_mcp_tool_id(server_name, tool_name)
|
| 399 |
+
mcp_mapping[tool_id] = (server_name, tool_name, config)
|
| 400 |
+
mcp_tools_list.append({
|
| 401 |
+
"type": "function",
|
| 402 |
+
"function": {
|
| 403 |
+
"name": tool_id,
|
| 404 |
+
"description": f"[from MCP server: {server_name}] {description}",
|
| 405 |
+
"parameters": input_schema
|
| 406 |
+
}
|
| 407 |
+
})
|
| 408 |
+
except Exception as e:
|
| 409 |
+
log_activity(f"[MCP Database Query Warning] Failed to load MCP tools: {e}")
|
| 410 |
+
return mcp_tools_list, mcp_mapping
|
| 411 |
+
|
| 412 |
+
async def execute_web_search(query: str) -> str:
|
| 413 |
+
tavily_key = os.environ.get("TAVILY_API_KEY", "")
|
| 414 |
+
if tavily_key:
|
| 415 |
+
try:
|
| 416 |
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 417 |
+
r = await client.post("https://api.tavily.com/search", json={
|
| 418 |
+
"api_key": tavily_key,
|
| 419 |
+
"query": query,
|
| 420 |
+
"search_depth": "basic",
|
| 421 |
+
"max_results": 5
|
| 422 |
+
})
|
| 423 |
+
if r.status_code == 200:
|
| 424 |
+
data = r.json()
|
| 425 |
+
results = []
|
| 426 |
+
for item in data.get("results", []):
|
| 427 |
+
results.append(f"Title: {item.get('title')}\nURL: {item.get('url')}\nContent: {item.get('content')}\n---")
|
| 428 |
+
return "\n".join(results) if results else "No results found."
|
| 429 |
+
except Exception as e:
|
| 430 |
+
log_activity(f"[Tavily Error] {e}")
|
| 431 |
+
|
| 432 |
+
exa_key = os.environ.get("EXA_API_KEY", "")
|
| 433 |
+
if exa_key:
|
| 434 |
+
try:
|
| 435 |
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 436 |
+
r = await client.post("https://api.exa.ai/search", headers={
|
| 437 |
+
"x-api-key": exa_key,
|
| 438 |
+
"Content-Type": "application/json"
|
| 439 |
+
}, json={
|
| 440 |
+
"query": query,
|
| 441 |
+
"numResults": 5,
|
| 442 |
+
"text": True
|
| 443 |
+
})
|
| 444 |
+
if r.status_code == 200:
|
| 445 |
+
data = r.json()
|
| 446 |
+
results = []
|
| 447 |
+
for item in data.get("results", []):
|
| 448 |
+
results.append(f"Title: {item.get('title')}\nURL: {item.get('url')}\nContent: {item.get('text', '')[:2000]}\n---")
|
| 449 |
+
return "\n".join(results) if results else "No results found."
|
| 450 |
+
except Exception as e:
|
| 451 |
+
log_activity(f"[Exa Error] {e}")
|
| 452 |
+
|
| 453 |
+
try:
|
| 454 |
+
escaped_query = urllib.parse.quote(query)
|
| 455 |
+
async with httpx.AsyncClient(headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}, timeout=10.0) as client:
|
| 456 |
+
r = await client.get(f"https://html.duckduckgo.com/html/?q={escaped_query}")
|
| 457 |
+
if r.status_code == 200:
|
| 458 |
+
try:
|
| 459 |
+
from bs4 import BeautifulSoup
|
| 460 |
+
soup = BeautifulSoup(r.text, 'html.parser')
|
| 461 |
+
results = []
|
| 462 |
+
for a in soup.find_all('a', class_='result__snippet')[:5]:
|
| 463 |
+
title_el = a.find_previous('a', class_='result__url')
|
| 464 |
+
title = title_el.text.strip() if title_el else "No Title"
|
| 465 |
+
url = title_el['href'] if title_el and 'href' in title_el.attrs else ""
|
| 466 |
+
snippet = a.text.strip()
|
| 467 |
+
results.append(f"Title: {title}\nURL: {url}\nSnippet: {snippet}\n---")
|
| 468 |
+
return "\n".join(results) if results else "No results found."
|
| 469 |
+
except Exception:
|
| 470 |
+
snippets = re.findall(r'<a class="result__snippet"[^>]*>(.*?)</a>', r.text, re.DOTALL)
|
| 471 |
+
results = []
|
| 472 |
+
for s in snippets[:5]:
|
| 473 |
+
clean_s = re.sub(r'<[^>]*>', '', s).strip()
|
| 474 |
+
results.append(f"Snippet: {clean_s}\n---")
|
| 475 |
+
return "\n".join(results) if results else "No results found."
|
| 476 |
+
except Exception as e:
|
| 477 |
+
log_activity(f"[DDG Scrape Error] {e}")
|
| 478 |
+
return "Error: Web search failed. Setup API keys (TAVILY_API_KEY, EXA_API_KEY) for best results."
|
| 479 |
+
|
| 480 |
+
async def execute_web_read(url: str) -> str:
|
| 481 |
+
jina_url = f"https://r.jina.ai/{url}"
|
| 482 |
+
try:
|
| 483 |
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
| 484 |
+
r = await client.get(jina_url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"})
|
| 485 |
+
if r.status_code == 200:
|
| 486 |
+
content = r.text
|
| 487 |
+
if len(content) > 30000:
|
| 488 |
+
content = content[:30000] + "\n\n[Truncated - webpage content is extremely long]"
|
| 489 |
+
return content
|
| 490 |
+
except Exception as e:
|
| 491 |
+
log_activity(f"[Jina Reader Error] {e}")
|
| 492 |
+
|
| 493 |
+
try:
|
| 494 |
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 495 |
+
r = await client.get(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"})
|
| 496 |
+
if r.status_code == 200:
|
| 497 |
+
html = r.text
|
| 498 |
+
clean = re.sub(r'<script.*?>.*?</script>', '', html, flags=re.DOTALL | re.IGNORECASE)
|
| 499 |
+
clean = re.sub(r'<style.*?>.*?</style>', '', clean, flags=re.DOTALL | re.IGNORECASE)
|
| 500 |
+
clean = re.sub(r'<.*?>', ' ', clean)
|
| 501 |
+
clean = re.sub(r'\s+', ' ', clean).strip()
|
| 502 |
+
if len(clean) > 15000:
|
| 503 |
+
clean = clean[:15000] + "\n\n[Truncated]"
|
| 504 |
+
return clean
|
| 505 |
+
except Exception as e:
|
| 506 |
+
log_activity(f"[Direct Scrape Error] {e}")
|
| 507 |
+
return f"Error: Failed to read URL {url}."
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
async def execute_tool(name: str, arguments: dict, mcp_mapping: dict = None) -> str:
|
| 511 |
"""Execute a tool and return its output as a string asynchronously."""
|
| 512 |
try:
|
| 513 |
if name == "read_file":
|
|
|
|
| 539 |
command,
|
| 540 |
stdout=asyncio.subprocess.PIPE,
|
| 541 |
stderr=asyncio.subprocess.PIPE,
|
| 542 |
+
cwd=workspace_var.get(),
|
| 543 |
env={**os.environ, "HOME": "/tmp", "PATH": os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin")},
|
| 544 |
)
|
| 545 |
|
|
|
|
| 596 |
"grep", "-rn", "--include=*", pattern, str(path),
|
| 597 |
stdout=asyncio.subprocess.PIPE,
|
| 598 |
stderr=asyncio.subprocess.PIPE,
|
| 599 |
+
cwd=workspace_var.get(),
|
| 600 |
)
|
| 601 |
try:
|
| 602 |
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=10)
|
|
|
|
| 613 |
output = output[:10000] + "\n\n[Truncated]"
|
| 614 |
return output
|
| 615 |
|
| 616 |
+
elif name == "web_search":
|
| 617 |
+
query = arguments.get("query")
|
| 618 |
+
return await execute_web_search(query)
|
| 619 |
+
|
| 620 |
+
elif name == "web_read":
|
| 621 |
+
url = arguments.get("url")
|
| 622 |
+
return await execute_web_read(url)
|
| 623 |
+
|
| 624 |
+
elif mcp_mapping and name in mcp_mapping:
|
| 625 |
+
server_name, original_tool_name, config = mcp_mapping[name]
|
| 626 |
+
try:
|
| 627 |
+
# Ensure registered on Space 4
|
| 628 |
+
await apost_json(f"{SPACE4_URL}/api/mcp/register", {
|
| 629 |
+
"name": server_name,
|
| 630 |
+
"config": config
|
| 631 |
+
})
|
| 632 |
+
# Call tool on Space 4
|
| 633 |
+
call_res = await apost_json(f"{SPACE4_URL}/api/mcp/call", {
|
| 634 |
+
"serverName": server_name,
|
| 635 |
+
"toolName": original_tool_name,
|
| 636 |
+
"arguments": arguments
|
| 637 |
+
})
|
| 638 |
+
if isinstance(call_res, dict) and "error" in call_res:
|
| 639 |
+
return f"Error from MCP server {server_name}: {call_res['error']}"
|
| 640 |
+
if isinstance(call_res, dict) and "content" in call_res:
|
| 641 |
+
texts = []
|
| 642 |
+
for item in call_res["content"]:
|
| 643 |
+
if isinstance(item, dict) and item.get("type") == "text":
|
| 644 |
+
texts.append(item.get("text", ""))
|
| 645 |
+
return "\n".join(texts)
|
| 646 |
+
return str(call_res)
|
| 647 |
+
except Exception as e:
|
| 648 |
+
return f"Error executing MCP tool {name}: {str(e)}"
|
| 649 |
+
|
| 650 |
else:
|
| 651 |
return f"Error: Unknown tool: {name}"
|
| 652 |
|
|
|
|
| 948 |
if user_msg:
|
| 949 |
await save_message(session_id, "user", user_msg.get("content", ""))
|
| 950 |
|
| 951 |
+
session_folder = session_id[:8] if session_id else "default"
|
| 952 |
+
active_workspace = os.path.join(WORKSPACE_DIR, session_folder)
|
| 953 |
+
os.makedirs(active_workspace, exist_ok=True)
|
| 954 |
+
|
| 955 |
if not stream:
|
| 956 |
# Non-streaming: simple completion
|
| 957 |
+
workspace_token = workspace_var.set(active_workspace)
|
| 958 |
try:
|
| 959 |
+
mcp_tools, mcp_mapping = await get_active_mcp_tools()
|
| 960 |
+
active_tools = list(TOOLS) + mcp_tools
|
| 961 |
kwargs = {"model": requested_model, "messages": final_messages}
|
| 962 |
if is_agentic:
|
| 963 |
+
kwargs["tools"] = active_tools
|
| 964 |
kwargs["tool_choice"] = "auto"
|
| 965 |
async with completions_semaphore:
|
| 966 |
response = await client.chat.completions.create(**kwargs)
|
|
|
|
| 978 |
except Exception as e:
|
| 979 |
ACTIVE_SESSIONS.discard(session_id)
|
| 980 |
return JSONResponse({"error": {"message": str(e), "type": "internal_error"}}, status_code=500)
|
| 981 |
+
finally:
|
| 982 |
+
workspace_var.reset(workspace_token)
|
| 983 |
|
| 984 |
# Streaming + agentic loop
|
| 985 |
async def generate() -> AsyncIterator[str]:
|
| 986 |
nonlocal final_messages
|
| 987 |
+
workspace_token = workspace_var.set(active_workspace)
|
| 988 |
+
try:
|
| 989 |
+
mcp_tools, mcp_mapping = await get_active_mcp_tools()
|
| 990 |
+
active_tools = list(TOOLS) + mcp_tools
|
| 991 |
+
async with completions_semaphore:
|
| 992 |
for round_num in range(MAX_TOOL_ROUNDS + 1):
|
| 993 |
# Perform auto-compaction before calling NIM API
|
| 994 |
final_messages = compact_history(final_messages)
|
| 995 |
kwargs = {"model": requested_model, "messages": final_messages, "stream": True}
|
| 996 |
if is_agentic:
|
| 997 |
+
kwargs["tools"] = active_tools
|
| 998 |
kwargs["tool_choice"] = "auto"
|
| 999 |
|
| 1000 |
# Collect streamed response
|
|
|
|
| 1088 |
yield make_chunk(request_id, requested_model, f"\n\n🔧 **{func_name}**")
|
| 1089 |
if repair_notes:
|
| 1090 |
yield make_chunk(request_id, requested_model, " *(Auto-Repaired)*")
|
|
|
|
| 1091 |
if func_name == "run_bash" and "command" in repaired_args:
|
| 1092 |
yield make_chunk(request_id, requested_model, f": `{repaired_args['command']}`\n")
|
| 1093 |
elif func_name == "read_file" and "path" in repaired_args:
|
|
|
|
| 1098 |
yield make_chunk(request_id, requested_model, f": `{repaired_args.get('path', '.')}`\n")
|
| 1099 |
elif func_name == "grep_search":
|
| 1100 |
yield make_chunk(request_id, requested_model, f": `{repaired_args.get('pattern', '')}`\n")
|
| 1101 |
+
elif func_name == "web_search" and "query" in repaired_args:
|
| 1102 |
+
yield make_chunk(request_id, requested_model, f": `{repaired_args['query']}`\n")
|
| 1103 |
+
elif func_name == "web_read" and "url" in repaired_args:
|
| 1104 |
+
yield make_chunk(request_id, requested_model, f": `{repaired_args['url']}`\n")
|
| 1105 |
+
elif mcp_mapping and func_name in mcp_mapping:
|
| 1106 |
+
yield make_chunk(request_id, requested_model, f": calling tool\n")
|
| 1107 |
else:
|
| 1108 |
yield make_chunk(request_id, requested_model, "\n")
|
| 1109 |
|
| 1110 |
# Execute the tool
|
| 1111 |
+
result = await execute_tool(func_name, repaired_args, mcp_mapping)
|
| 1112 |
|
| 1113 |
# Append teaching note if repaired
|
| 1114 |
if repair_notes:
|
|
|
|
| 1134 |
yield make_chunk(request_id, requested_model, finish_reason="stop")
|
| 1135 |
yield "data: [DONE]\n\n"
|
| 1136 |
|
| 1137 |
+
except Exception as e:
|
| 1138 |
+
error_msg = f"\n\n❌ Error: {str(e)}"
|
| 1139 |
+
yield make_chunk(request_id, requested_model, error_msg)
|
| 1140 |
+
yield make_chunk(request_id, requested_model, finish_reason="stop")
|
| 1141 |
+
yield "data: [DONE]\n\n"
|
| 1142 |
+
finally:
|
| 1143 |
+
workspace_var.reset(workspace_token)
|
| 1144 |
|
| 1145 |
return StreamingResponse(
|
| 1146 |
generate(),
|
|
|
|
| 1213 |
}
|
| 1214 |
|
| 1215 |
try:
|
| 1216 |
+
active_project = ""
|
| 1217 |
+
if db_pool:
|
| 1218 |
+
try:
|
| 1219 |
+
async with db_pool.acquire() as conn:
|
| 1220 |
+
row = await conn.fetchrow("SELECT project_name FROM eternity_projects WHERE is_active = true ORDER BY id DESC LIMIT 1")
|
| 1221 |
+
if row:
|
| 1222 |
+
active_project = row["project_name"].replace(" ", "-").lower()
|
| 1223 |
+
except Exception:
|
| 1224 |
+
pass
|
| 1225 |
+
|
| 1226 |
+
project_folder = active_project if active_project else "default"
|
| 1227 |
+
w_path = (Path(WORKSPACE_DIR) / project_folder).resolve()
|
| 1228 |
if not w_path.exists():
|
| 1229 |
w_path.mkdir(parents=True, exist_ok=True)
|
| 1230 |
return build_tree(w_path, w_path)
|
|
|
|
| 1235 |
@app.get("/api/workspace/file")
|
| 1236 |
async def get_workspace_file(path: str):
|
| 1237 |
try:
|
| 1238 |
+
active_project = ""
|
| 1239 |
+
if db_pool:
|
| 1240 |
+
try:
|
| 1241 |
+
async with db_pool.acquire() as conn:
|
| 1242 |
+
row = await conn.fetchrow("SELECT project_name FROM eternity_projects WHERE is_active = true ORDER BY id DESC LIMIT 1")
|
| 1243 |
+
if row:
|
| 1244 |
+
active_project = row["project_name"].replace(" ", "-").lower()
|
| 1245 |
+
except Exception:
|
| 1246 |
+
pass
|
| 1247 |
+
|
| 1248 |
+
project_folder = active_project if active_project else "default"
|
| 1249 |
+
w_path = (Path(WORKSPACE_DIR) / project_folder).resolve()
|
| 1250 |
+
token = workspace_var.set(str(w_path))
|
| 1251 |
+
try:
|
| 1252 |
+
safe_p = _safe_path(path)
|
| 1253 |
+
if not safe_p.exists() or not safe_p.is_file():
|
| 1254 |
+
raise HTTPException(status_code=404, detail="File not found")
|
| 1255 |
+
content = safe_p.read_text(encoding="utf-8", errors="replace")
|
| 1256 |
+
return {"path": path, "content": content}
|
| 1257 |
+
finally:
|
| 1258 |
+
workspace_var.reset(token)
|
| 1259 |
except Exception as e:
|
| 1260 |
raise HTTPException(status_code=500, detail=str(e))
|
| 1261 |
|
|
|
|
| 1263 |
@app.get("/api/workspace/latest-screenshot")
|
| 1264 |
async def get_latest_screenshot():
|
| 1265 |
try:
|
| 1266 |
+
active_project = ""
|
| 1267 |
+
if db_pool:
|
| 1268 |
+
try:
|
| 1269 |
+
async with db_pool.acquire() as conn:
|
| 1270 |
+
row = await conn.fetchrow("SELECT project_name FROM eternity_projects WHERE is_active = true ORDER BY id DESC LIMIT 1")
|
| 1271 |
+
if row:
|
| 1272 |
+
active_project = row["project_name"].replace(" ", "-").lower()
|
| 1273 |
+
except Exception:
|
| 1274 |
+
pass
|
| 1275 |
+
|
| 1276 |
+
project_folder = active_project if active_project else "default"
|
| 1277 |
+
w_path = (Path(WORKSPACE_DIR) / project_folder).resolve()
|
| 1278 |
png_files = []
|
| 1279 |
for p in w_path.rglob("*.png"):
|
| 1280 |
if any(part.startswith(".") for part in p.parts):
|
|
|
|
| 2044 |
async def forge_execute(req: ForgeExecuteRequest, authorization: str = Header(None)):
|
| 2045 |
auth(authorization)
|
| 2046 |
log_activity(f"[Forge] Received execution request for task {req.task_id}: '{req.prompt[:80]}'")
|
| 2047 |
+
|
| 2048 |
+
project_folder = "default"
|
| 2049 |
+
if "_" in req.task_id:
|
| 2050 |
+
parts = req.task_id.split("_")
|
| 2051 |
+
if len(parts) >= 2:
|
| 2052 |
+
project_folder = parts[1].replace(" ", "-").lower()
|
| 2053 |
+
active_workspace = os.path.join(WORKSPACE_DIR, project_folder)
|
| 2054 |
+
os.makedirs(active_workspace, exist_ok=True)
|
| 2055 |
+
workspace_token = workspace_var.set(active_workspace)
|
| 2056 |
+
|
| 2057 |
try:
|
| 2058 |
+
# Load active MCP tools
|
| 2059 |
+
mcp_tools, mcp_mapping = await get_active_mcp_tools()
|
| 2060 |
+
active_tools = list(TOOLS) + mcp_tools
|
| 2061 |
+
|
| 2062 |
# Inject project_state.md into system context if provided (Karpathy protocol)
|
| 2063 |
system_content = AGENTIC_SYSTEM_PROMPT + f"\nContext rules: {req.context_rules}"
|
| 2064 |
if req.project_state_md:
|
|
|
|
| 2081 |
response = await nim_client.chat.completions.create(
|
| 2082 |
model=RECOMMENDED_MODEL,
|
| 2083 |
messages=final_messages,
|
| 2084 |
+
tools=active_tools,
|
| 2085 |
tool_choice="auto",
|
| 2086 |
timeout=8.0
|
| 2087 |
)
|
|
|
|
| 2092 |
response = await mistral_client.chat.completions.create(
|
| 2093 |
model="mistral-large-latest",
|
| 2094 |
messages=final_messages,
|
| 2095 |
+
tools=active_tools,
|
| 2096 |
tool_choice="auto"
|
| 2097 |
)
|
| 2098 |
else:
|
|
|
|
| 2142 |
func_args = json.loads(repaired_str)
|
| 2143 |
except Exception:
|
| 2144 |
func_args = {}
|
| 2145 |
+
result = await execute_tool(func_name, func_args, mcp_mapping)
|
| 2146 |
|
| 2147 |
final_messages.append({
|
| 2148 |
"role": "tool",
|
|
|
|
| 2170 |
"status": "error",
|
| 2171 |
"error": str(e)
|
| 2172 |
}
|
| 2173 |
+
finally:
|
| 2174 |
+
workspace_var.reset(workspace_token)
|
|
|
|
|
|
|
|
|
|
| 2175 |
|
| 2176 |
async def db_heartbeat_loop():
|
| 2177 |
log_activity("Database Heartbeat task started")
|
|
|
|
| 2821 |
|
| 2822 |
|
| 2823 |
def run_backup_loop():
|
| 2824 |
+
if os.environ.get("SPACE_NAME", "space2-cerebrum") == "space2-cerebrum":
|
| 2825 |
+
log_activity("Local Git Backup Loop disabled on Cerebrum (Space 2)")
|
| 2826 |
+
return
|
| 2827 |
log_activity("Local Git Backup Loop started")
|
| 2828 |
while True:
|
| 2829 |
# Wait 5 minutes between runs
|