forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagent_tools.py
More file actions
179 lines (146 loc) · 6.16 KB
/
Copy pathagent_tools.py
File metadata and controls
179 lines (146 loc) · 6.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
"""
Agent tools router — exposes Python backend tools to agents.
Endpoints:
- GET /v1/agent/tools — returns tool definitions (name, description, parameters)
- POST /v1/agent/execute-tool — executes a named tool and returns the result
"""
import logging
from typing import Any
from utils.executors import db_executor, run_blocking
from utils.jit_rollout import JITDecisionStage, resolve_jit_rollout, resolve_jit_rollout_sync
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from utils.other.endpoints import get_current_user_uid, with_rate_limit
from utils.retrieval.agentic import agent_config_context, CORE_TOOLS, JIT_ONLY_TOOL_NAMES
from utils.retrieval.tool_result_boundaries import preserve_chat_memory_tool_result_boundary
from utils.retrieval.tools.app_tools import load_app_tools
from utils.observability.fallback import record_fallback
logger = logging.getLogger(__name__)
router = APIRouter()
class AgentToolSchema(BaseModel):
name: str
description: str
parameters: dict[str, Any]
class AgentToolsResponse(BaseModel):
tools: list[AgentToolSchema]
def _tool_schema(t) -> dict:
"""Extract a clean JSON schema from a LangChain tool."""
schema = t.args_schema.model_json_schema() if t.args_schema else {}
props = schema.get("properties", {})
required = list(schema.get("required", []))
# Strip the 'config' parameter — it's internal LangChain plumbing
props.pop("config", None)
if "config" in required:
required.remove("config")
return {
"name": t.name,
"description": t.description or "",
"parameters": {
"type": "object",
"properties": props,
"required": required,
},
}
@router.get("/v1/agent/tools", response_model=AgentToolsResponse)
def list_tools(uid: str = Depends(get_current_user_uid)):
"""Return all available tool definitions for a user."""
tools = []
jit_tools_enabled = resolve_jit_rollout_sync(uid, stage=JITDecisionStage.READ_ONLY).permits_work
for t in CORE_TOOLS:
if t.name in JIT_ONLY_TOOL_NAMES and not jit_tools_enabled:
continue
tools.append(_tool_schema(t))
degraded = False
try:
app_tools = load_app_tools(uid)
except Exception:
# Whole app-tool lane unavailable — core tools still serve.
logger.error("⚠️ Error loading app tools for agent_tools", exc_info=True)
app_tools = []
degraded = True
for t in app_tools:
try:
tools.append(_tool_schema(t))
except Exception:
# One malformed schema must not drop the remaining app tools.
logger.error(f"⚠️ Skipping app tool with malformed schema: {getattr(t, 'name', '?')}", exc_info=True)
degraded = True
if degraded:
record_fallback(
component='agent_tools',
from_mode='full_toolset',
to_mode='partial_toolset',
reason='malformed_doc',
outcome='degraded',
)
return {"tools": tools}
class ExecuteToolRequest(BaseModel):
tool_name: str
params: dict = {}
class ExecuteToolResponse(BaseModel):
result: str | None = None
error: str | None = None
@router.post("/v1/agent/execute-tool", response_model=ExecuteToolResponse)
async def execute_tool(
body: ExecuteToolRequest,
uid: str = Depends(with_rate_limit(get_current_user_uid, "agent:execute_tool")),
):
"""Execute a named tool and return its result."""
if body.tool_name in JIT_ONLY_TOOL_NAMES:
rollout = await resolve_jit_rollout(
uid,
stage=JITDecisionStage.READ_ONLY,
force_refresh=True,
)
if not rollout.permits_work:
raise HTTPException(status_code=404, detail=f"Tool '{body.tool_name}' not found")
# Set up agent_config_context so tools can resolve the UID
config = {
"configurable": {
"user_id": uid,
},
}
agent_config_context.set(config)
# Find the tool. `load_app_tools` reads Redis plus one Firestore document
# per enabled app, so it must not run on the event loop — see the canonical
# path in utils/retrieval/agentic.py.
all_tools = list(CORE_TOOLS)
try:
app_tools = await run_blocking(db_executor, load_app_tools, uid)
all_tools.extend(app_tools)
except Exception as error:
logger.error("⚠️ Error loading app tools error_type=%s", type(error).__name__)
record_fallback(
component='agent_tools',
from_mode='full_toolset',
to_mode='partial_toolset',
reason='other',
outcome='degraded',
)
target = None
for t in all_tools:
if t.name == body.tool_name:
target = t
break
if target is None:
raise HTTPException(status_code=404, detail=f"Tool '{body.tool_name}' not found")
# Strip config param if caller accidentally included it
params = {k: v for k, v in body.params.items() if k != "config"}
try:
# Prefer async coroutine if available (app tools), else sync invoke
if hasattr(target, "coroutine") and target.coroutine is not None:
result = await target.coroutine(**params)
else:
# Every CORE_TOOLS entry is a sync @tool that fans out to Firestore
# and Pinecone, so invoking it here would park the whole event loop
# for the duration. `run_blocking` copies the current context, so
# `agent_config_context` still resolves inside the worker thread.
# Pass config as second arg (LangChain RunnableConfig), not as tool input
result = await run_blocking(db_executor, target.invoke, params, config=config)
result = preserve_chat_memory_tool_result_boundary(body.tool_name, str(result))
return {"result": result}
except Exception as error:
# Exception text can embed caller params (pydantic renders
# `input_value=...`), so keep it off both the log and response planes.
logger.error("❌ Error executing tool %s error_type=%s", body.tool_name, type(error).__name__)
return {"error": "Tool execution failed"}