rossum-agent 1.0.0rc0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rossum_agent/__init__.py +9 -0
- rossum_agent/agent/__init__.py +32 -0
- rossum_agent/agent/core.py +932 -0
- rossum_agent/agent/memory.py +176 -0
- rossum_agent/agent/models.py +160 -0
- rossum_agent/agent/request_classifier.py +152 -0
- rossum_agent/agent/skills.py +132 -0
- rossum_agent/agent/types.py +5 -0
- rossum_agent/agent_logging.py +56 -0
- rossum_agent/api/__init__.py +1 -0
- rossum_agent/api/cli.py +51 -0
- rossum_agent/api/dependencies.py +190 -0
- rossum_agent/api/main.py +180 -0
- rossum_agent/api/models/__init__.py +1 -0
- rossum_agent/api/models/schemas.py +301 -0
- rossum_agent/api/routes/__init__.py +1 -0
- rossum_agent/api/routes/chats.py +95 -0
- rossum_agent/api/routes/files.py +113 -0
- rossum_agent/api/routes/health.py +44 -0
- rossum_agent/api/routes/messages.py +218 -0
- rossum_agent/api/services/__init__.py +1 -0
- rossum_agent/api/services/agent_service.py +451 -0
- rossum_agent/api/services/chat_service.py +197 -0
- rossum_agent/api/services/file_service.py +65 -0
- rossum_agent/assets/Primary_light_logo.png +0 -0
- rossum_agent/bedrock_client.py +64 -0
- rossum_agent/prompts/__init__.py +27 -0
- rossum_agent/prompts/base_prompt.py +80 -0
- rossum_agent/prompts/system_prompt.py +24 -0
- rossum_agent/py.typed +0 -0
- rossum_agent/redis_storage.py +482 -0
- rossum_agent/rossum_mcp_integration.py +123 -0
- rossum_agent/skills/hook-debugging.md +31 -0
- rossum_agent/skills/organization-setup.md +60 -0
- rossum_agent/skills/rossum-deployment.md +102 -0
- rossum_agent/skills/schema-patching.md +61 -0
- rossum_agent/skills/schema-pruning.md +23 -0
- rossum_agent/skills/ui-settings.md +45 -0
- rossum_agent/streamlit_app/__init__.py +1 -0
- rossum_agent/streamlit_app/app.py +646 -0
- rossum_agent/streamlit_app/beep_sound.py +36 -0
- rossum_agent/streamlit_app/cli.py +17 -0
- rossum_agent/streamlit_app/render_modules.py +123 -0
- rossum_agent/streamlit_app/response_formatting.py +305 -0
- rossum_agent/tools/__init__.py +214 -0
- rossum_agent/tools/core.py +173 -0
- rossum_agent/tools/deploy.py +404 -0
- rossum_agent/tools/dynamic_tools.py +365 -0
- rossum_agent/tools/file_tools.py +62 -0
- rossum_agent/tools/formula.py +187 -0
- rossum_agent/tools/skills.py +31 -0
- rossum_agent/tools/spawn_mcp.py +227 -0
- rossum_agent/tools/subagents/__init__.py +31 -0
- rossum_agent/tools/subagents/base.py +303 -0
- rossum_agent/tools/subagents/hook_debug.py +591 -0
- rossum_agent/tools/subagents/knowledge_base.py +305 -0
- rossum_agent/tools/subagents/mcp_helpers.py +47 -0
- rossum_agent/tools/subagents/schema_patching.py +471 -0
- rossum_agent/url_context.py +167 -0
- rossum_agent/user_detection.py +100 -0
- rossum_agent/utils.py +128 -0
- rossum_agent-1.0.0rc0.dist-info/METADATA +311 -0
- rossum_agent-1.0.0rc0.dist-info/RECORD +67 -0
- rossum_agent-1.0.0rc0.dist-info/WHEEL +5 -0
- rossum_agent-1.0.0rc0.dist-info/entry_points.txt +3 -0
- rossum_agent-1.0.0rc0.dist-info/licenses/LICENSE +21 -0
- rossum_agent-1.0.0rc0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
"""Hook debugging sub-agent.
|
|
2
|
+
|
|
3
|
+
Provides tools for debugging Rossum Python function hooks, including sandboxed execution
|
|
4
|
+
and iterative debugging with the Opus sub-agent.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import builtins
|
|
10
|
+
import collections
|
|
11
|
+
import datetime as dt
|
|
12
|
+
import decimal
|
|
13
|
+
import functools
|
|
14
|
+
import io
|
|
15
|
+
import itertools
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import math
|
|
19
|
+
import re
|
|
20
|
+
import string
|
|
21
|
+
import time
|
|
22
|
+
import traceback
|
|
23
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
24
|
+
from decimal import Decimal, InvalidOperation
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from anthropic import beta_tool
|
|
31
|
+
|
|
32
|
+
from rossum_agent.tools.subagents.base import SubAgent, SubAgentConfig, SubAgentResult
|
|
33
|
+
from rossum_agent.tools.subagents.knowledge_base import (
|
|
34
|
+
WebSearchError,
|
|
35
|
+
_call_opus_for_web_search_analysis,
|
|
36
|
+
search_knowledge_base,
|
|
37
|
+
)
|
|
38
|
+
from rossum_agent.tools.subagents.mcp_helpers import call_mcp_tool
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
_WEB_SEARCH_NO_RESULTS = "__NO_RESULTS__"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _extract_web_search_text_from_block(block: Any) -> str | None:
|
|
46
|
+
"""Extract full web search results text from a single web_search_tool_result block.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
block: The response content block to process.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Formatted text with full search results, _WEB_SEARCH_NO_RESULTS if search
|
|
53
|
+
returned empty, or None if not a web search block.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
WebSearchError: If web search returned an error.
|
|
57
|
+
"""
|
|
58
|
+
if not (hasattr(block, "type") and block.type == "web_search_tool_result"):
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
search_results_text = []
|
|
62
|
+
if hasattr(block, "content") and block.content:
|
|
63
|
+
for result in block.content:
|
|
64
|
+
result_type = getattr(result, "type", None)
|
|
65
|
+
|
|
66
|
+
if result_type == "web_search_result_error":
|
|
67
|
+
error_code = getattr(result, "error_code", "unknown")
|
|
68
|
+
error_message = getattr(result, "message", "Web search failed")
|
|
69
|
+
logger.error(f"Web search error: code={error_code}, message={error_message}")
|
|
70
|
+
raise WebSearchError(f"Web search failed: {error_code} - {error_message}")
|
|
71
|
+
|
|
72
|
+
if result_type == "web_search_result":
|
|
73
|
+
title = getattr(result, "title", "")
|
|
74
|
+
url = getattr(result, "url", "")
|
|
75
|
+
page_content = getattr(result, "page_content", "")
|
|
76
|
+
search_results_text.append(f"## {title}\nURL: {url}\n\n{page_content}\n")
|
|
77
|
+
|
|
78
|
+
if not search_results_text:
|
|
79
|
+
logger.warning("Web search returned no results for the query")
|
|
80
|
+
return _WEB_SEARCH_NO_RESULTS
|
|
81
|
+
|
|
82
|
+
return "\n---\n".join(search_results_text)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _extract_and_analyze_web_search_results(block: Any, iteration: int, max_iterations: int) -> dict[str, Any] | None:
|
|
86
|
+
"""Extract web search results and analyze with Opus sub-agent.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
block: The response content block to process.
|
|
90
|
+
iteration: Current iteration number for logging.
|
|
91
|
+
max_iterations: Maximum iterations for logging.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Tool result dict with analyzed search results, or None if not a web search block.
|
|
95
|
+
"""
|
|
96
|
+
full_results = _extract_web_search_text_from_block(block)
|
|
97
|
+
if full_results is None:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
if full_results == _WEB_SEARCH_NO_RESULTS:
|
|
101
|
+
logger.info(f"debug_hook sub-agent [iter {iteration}/{max_iterations}]: web search returned no results")
|
|
102
|
+
return {
|
|
103
|
+
"type": "tool_result",
|
|
104
|
+
"tool_use_id": block.id,
|
|
105
|
+
"content": "Web search returned no results for the query.",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
logger.info(f"debug_hook sub-agent [iter {iteration}/{max_iterations}]: processing web search results")
|
|
109
|
+
query = getattr(block, "search_query", "Rossum documentation")
|
|
110
|
+
logger.info(f"debug_hook sub-agent [iter {iteration}/{max_iterations}]: analyzing with Opus sub-agent")
|
|
111
|
+
analyzed_results = _call_opus_for_web_search_analysis(query, full_results)
|
|
112
|
+
return {
|
|
113
|
+
"type": "tool_result",
|
|
114
|
+
"tool_use_id": block.id,
|
|
115
|
+
"content": f"Analyzed Rossum Knowledge Base search results:\n\n{analyzed_results}",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_ALLOWED_BUILTIN_NAMES = {
|
|
120
|
+
"abs",
|
|
121
|
+
"all",
|
|
122
|
+
"any",
|
|
123
|
+
"bool",
|
|
124
|
+
"dict",
|
|
125
|
+
"enumerate",
|
|
126
|
+
"filter",
|
|
127
|
+
"float",
|
|
128
|
+
"frozenset",
|
|
129
|
+
"getattr",
|
|
130
|
+
"hasattr",
|
|
131
|
+
"int",
|
|
132
|
+
"isinstance",
|
|
133
|
+
"iter",
|
|
134
|
+
"len",
|
|
135
|
+
"list",
|
|
136
|
+
"map",
|
|
137
|
+
"max",
|
|
138
|
+
"min",
|
|
139
|
+
"next",
|
|
140
|
+
"pow",
|
|
141
|
+
"range",
|
|
142
|
+
"repr",
|
|
143
|
+
"reversed",
|
|
144
|
+
"round",
|
|
145
|
+
"set",
|
|
146
|
+
"sorted",
|
|
147
|
+
"str",
|
|
148
|
+
"sum",
|
|
149
|
+
"tuple",
|
|
150
|
+
"zip",
|
|
151
|
+
"Exception",
|
|
152
|
+
"ValueError",
|
|
153
|
+
"TypeError",
|
|
154
|
+
"KeyError",
|
|
155
|
+
"IndexError",
|
|
156
|
+
"RuntimeError",
|
|
157
|
+
"AttributeError",
|
|
158
|
+
"print",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_HOOK_DEBUG_SYSTEM_PROMPT = """Goal: Debug Rossum hook by fetching code/data, identifying ALL issues, fixing iteratively with evaluate_python_hook.
|
|
162
|
+
|
|
163
|
+
## Workflow
|
|
164
|
+
|
|
165
|
+
1. get_hook → fetch code (in config.code)
|
|
166
|
+
2. get_annotation → fetch annotation data (content for datapoints)
|
|
167
|
+
3. evaluate_python_hook → execute and see errors
|
|
168
|
+
4. Fix all issues found
|
|
169
|
+
5. Verify with evaluate_python_hook (status="success")
|
|
170
|
+
6. Keep iterating until robust
|
|
171
|
+
|
|
172
|
+
Must call evaluate_python_hook at least once before final answer.
|
|
173
|
+
|
|
174
|
+
## Tools
|
|
175
|
+
|
|
176
|
+
| Tool | Purpose |
|
|
177
|
+
|------|---------|
|
|
178
|
+
| get_hook | Fetch hook code by ID |
|
|
179
|
+
| get_annotation | Fetch annotation data by ID |
|
|
180
|
+
| get_schema | Optionally fetch schema |
|
|
181
|
+
| evaluate_python_hook | Execute code against annotation |
|
|
182
|
+
| web_search | Search Rossum KB for docs |
|
|
183
|
+
|
|
184
|
+
## Hook Structure
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
def rossum_hook_request_handler(payload):
|
|
188
|
+
annotation = payload["annotation"] # content has datapoints
|
|
189
|
+
schema = payload.get("schema")
|
|
190
|
+
# Return: {"operations": [...]} or {"messages": [...]}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
| Pattern | Format |
|
|
194
|
+
|---------|--------|
|
|
195
|
+
| Field access | `annotation["content"]` → datapoints with schema_id, value, id |
|
|
196
|
+
| Field update | `{"operations": [{"op": "replace", "id": <numeric_id>, "value": {...}}]}` |
|
|
197
|
+
| Messages | `{"messages": [{"type": "error", "content": "..."}]}` |
|
|
198
|
+
|
|
199
|
+
## Environment Constraints
|
|
200
|
+
|
|
201
|
+
- No imports or external I/O
|
|
202
|
+
- Available: collections, datetime, decimal (Decimal, InvalidOperation), functools, itertools, json, math, re, string
|
|
203
|
+
|
|
204
|
+
## Issue Categories
|
|
205
|
+
|
|
206
|
+
| Category | Check |
|
|
207
|
+
|----------|-------|
|
|
208
|
+
| Syntax | Typos, invalid Python |
|
|
209
|
+
| Null handling | Empty strings, None values |
|
|
210
|
+
| Type conversion | Decimal from strings/None |
|
|
211
|
+
| Field access | Missing fields, wrong keys |
|
|
212
|
+
| Logic | Calculation errors |
|
|
213
|
+
| Edge cases | Empty lists, zero values |
|
|
214
|
+
| Return format | Correct structure |
|
|
215
|
+
|
|
216
|
+
## Common Pitfalls
|
|
217
|
+
|
|
218
|
+
| Issue | Solution |
|
|
219
|
+
|-------|----------|
|
|
220
|
+
| Decimal conversion | `Decimal(value) if value else Decimal(0)` or try/except |
|
|
221
|
+
| Missing fields | Check existence before access |
|
|
222
|
+
| Type mismatch | Field values are strings—convert explicitly |
|
|
223
|
+
|
|
224
|
+
## Output Format
|
|
225
|
+
|
|
226
|
+
1. Hook purpose (brief)
|
|
227
|
+
2. All issues found
|
|
228
|
+
3. Root causes
|
|
229
|
+
4. Fixed code (verified)
|
|
230
|
+
5. Successful execution result"""
|
|
231
|
+
|
|
232
|
+
_GET_HOOK_TOOL: dict[str, Any] = {
|
|
233
|
+
"name": "get_hook",
|
|
234
|
+
"description": "Fetch a Rossum hook by ID. Returns the hook object with config.code containing the Python code.",
|
|
235
|
+
"input_schema": {
|
|
236
|
+
"type": "object",
|
|
237
|
+
"properties": {"hook_id": {"type": "string", "description": "The hook ID (numeric string)"}},
|
|
238
|
+
"required": ["hook_id"],
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_GET_ANNOTATION_TOOL: dict[str, Any] = {
|
|
243
|
+
"name": "get_annotation",
|
|
244
|
+
"description": "Fetch a Rossum annotation by ID. Returns the annotation object with content containing datapoints.",
|
|
245
|
+
"input_schema": {
|
|
246
|
+
"type": "object",
|
|
247
|
+
"properties": {"annotation_id": {"type": "string", "description": "The annotation ID (numeric string)"}},
|
|
248
|
+
"required": ["annotation_id"],
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_GET_SCHEMA_TOOL: dict[str, Any] = {
|
|
253
|
+
"name": "get_schema",
|
|
254
|
+
"description": "Fetch a Rossum schema by ID. Returns the schema definition.",
|
|
255
|
+
"input_schema": {
|
|
256
|
+
"type": "object",
|
|
257
|
+
"properties": {"schema_id": {"type": "string", "description": "The schema ID (numeric string)"}},
|
|
258
|
+
"required": ["schema_id"],
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_EVALUATE_HOOK_TOOL: dict[str, Any] = {
|
|
263
|
+
"name": "evaluate_python_hook",
|
|
264
|
+
"description": (
|
|
265
|
+
"Execute Rossum hook Python code against annotation/schema data. "
|
|
266
|
+
"Returns JSON with status, result, stdout, stderr, and exception info."
|
|
267
|
+
),
|
|
268
|
+
"input_schema": {
|
|
269
|
+
"type": "object",
|
|
270
|
+
"properties": {
|
|
271
|
+
"code": {
|
|
272
|
+
"type": "string",
|
|
273
|
+
"description": "Full Python source with rossum_hook_request_handler(payload) function",
|
|
274
|
+
},
|
|
275
|
+
"annotation_json": {"type": "string", "description": "JSON string of annotation data"},
|
|
276
|
+
"schema_json": {"type": "string", "description": "Optional JSON string of schema data"},
|
|
277
|
+
},
|
|
278
|
+
"required": ["code", "annotation_json"],
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_SEARCH_KNOWLEDGE_BASE_TOOL: dict[str, Any] = {
|
|
283
|
+
"name": "search_knowledge_base",
|
|
284
|
+
"description": (
|
|
285
|
+
"Search the Rossum Knowledge Base (https://knowledge-base.rossum.ai/docs) "
|
|
286
|
+
"for documentation about extensions, hooks, configurations, and best practices. "
|
|
287
|
+
"Use this tool to find information about Rossum features, troubleshoot errors, "
|
|
288
|
+
"and understand extension configurations."
|
|
289
|
+
),
|
|
290
|
+
"input_schema": {
|
|
291
|
+
"type": "object",
|
|
292
|
+
"properties": {
|
|
293
|
+
"query": {
|
|
294
|
+
"type": "string",
|
|
295
|
+
"description": (
|
|
296
|
+
"Search query. Be specific - include extension names, error messages, "
|
|
297
|
+
"or feature names. Examples: 'document splitting extension', "
|
|
298
|
+
"'duplicate handling configuration', 'webhook timeout error'"
|
|
299
|
+
),
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
"required": ["query"],
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_OPUS_TOOLS: list[dict[str, Any]] = [
|
|
307
|
+
_GET_HOOK_TOOL,
|
|
308
|
+
_GET_ANNOTATION_TOOL,
|
|
309
|
+
_GET_SCHEMA_TOOL,
|
|
310
|
+
_EVALUATE_HOOK_TOOL,
|
|
311
|
+
_SEARCH_KNOWLEDGE_BASE_TOOL,
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _strip_imports(code: str) -> str:
|
|
316
|
+
"""Strip import statements from code since they're not allowed in sandbox."""
|
|
317
|
+
result = []
|
|
318
|
+
for line in code.split("\n"):
|
|
319
|
+
stripped = line.strip()
|
|
320
|
+
if stripped.startswith("import ") or stripped.startswith("from "):
|
|
321
|
+
continue
|
|
322
|
+
result.append(line)
|
|
323
|
+
return "\n".join(result)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _make_evaluate_response(
|
|
327
|
+
status: str,
|
|
328
|
+
start_time: float,
|
|
329
|
+
result: Any = None,
|
|
330
|
+
stdout: str = "",
|
|
331
|
+
stderr: str = "",
|
|
332
|
+
exc: BaseException | None = None,
|
|
333
|
+
) -> str:
|
|
334
|
+
"""Create a JSON response for evaluate_python_hook."""
|
|
335
|
+
exc_info: dict[str, str] | None
|
|
336
|
+
if exc is not None:
|
|
337
|
+
exc_info = {"type": exc.__class__.__name__, "message": str(exc), "traceback": traceback.format_exc()}
|
|
338
|
+
else:
|
|
339
|
+
exc_info = None
|
|
340
|
+
|
|
341
|
+
payload = {
|
|
342
|
+
"status": status,
|
|
343
|
+
"result": result,
|
|
344
|
+
"stdout": stdout,
|
|
345
|
+
"stderr": stderr,
|
|
346
|
+
"exception": exc_info,
|
|
347
|
+
"elapsed_ms": round((time.perf_counter() - start_time) * 1000, 3),
|
|
348
|
+
}
|
|
349
|
+
return json.dumps(payload, ensure_ascii=False, default=str)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _execute_opus_tool(tool_name: str, tool_input: dict[str, Any]) -> str:
|
|
353
|
+
"""Execute a tool for the Opus sub-agent."""
|
|
354
|
+
if tool_name == "evaluate_python_hook":
|
|
355
|
+
return evaluate_python_hook(
|
|
356
|
+
code=tool_input.get("code", ""),
|
|
357
|
+
annotation_json=tool_input.get("annotation_json", ""),
|
|
358
|
+
schema_json=tool_input.get("schema_json"),
|
|
359
|
+
)
|
|
360
|
+
if tool_name == "search_knowledge_base":
|
|
361
|
+
if not (query := tool_input.get("query", "")):
|
|
362
|
+
return json.dumps({"status": "error", "message": "Query is required"})
|
|
363
|
+
return search_knowledge_base(query)
|
|
364
|
+
if tool_name in ("get_hook", "get_annotation", "get_schema"):
|
|
365
|
+
mcp_result = call_mcp_tool(tool_name, tool_input)
|
|
366
|
+
return json.dumps(mcp_result, indent=2, default=str) if mcp_result else "No data returned"
|
|
367
|
+
return f"Unknown tool: {tool_name}"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class HookDebugSubAgent(SubAgent):
|
|
371
|
+
"""Sub-agent for hook debugging with sandboxed execution and web search."""
|
|
372
|
+
|
|
373
|
+
def __init__(self) -> None:
|
|
374
|
+
config = SubAgentConfig(
|
|
375
|
+
tool_name="debug_hook",
|
|
376
|
+
system_prompt=_HOOK_DEBUG_SYSTEM_PROMPT,
|
|
377
|
+
tools=_OPUS_TOOLS,
|
|
378
|
+
max_iterations=15,
|
|
379
|
+
max_tokens=16384,
|
|
380
|
+
)
|
|
381
|
+
super().__init__(config)
|
|
382
|
+
|
|
383
|
+
def execute_tool(self, tool_name: str, tool_input: dict[str, Any]) -> str:
|
|
384
|
+
"""Execute a tool call from the LLM."""
|
|
385
|
+
result = _execute_opus_tool(tool_name, tool_input)
|
|
386
|
+
|
|
387
|
+
if tool_name == "evaluate_python_hook":
|
|
388
|
+
try:
|
|
389
|
+
result_obj = json.loads(result)
|
|
390
|
+
status = result_obj.get("status", "unknown")
|
|
391
|
+
exc_type = result_obj.get("exception", {}).get("type") if result_obj.get("exception") else None
|
|
392
|
+
log_msg = f"evaluate_python_hook returned status='{status}'" + (
|
|
393
|
+
f", exception={exc_type}" if exc_type else ""
|
|
394
|
+
)
|
|
395
|
+
logger.info(f"debug_hook sub-agent: {log_msg}")
|
|
396
|
+
except Exception:
|
|
397
|
+
logger.debug("Failed to parse tool result for logging")
|
|
398
|
+
|
|
399
|
+
return result
|
|
400
|
+
|
|
401
|
+
def process_response_block(self, block: Any, iteration: int, max_iterations: int) -> dict[str, Any] | None:
|
|
402
|
+
"""Process web search result blocks."""
|
|
403
|
+
return _extract_and_analyze_web_search_results(block, iteration, max_iterations)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _call_opus_for_debug(hook_id: str, annotation_id: str, schema_id: str | None) -> SubAgentResult:
|
|
407
|
+
"""Call Opus model for hook debugging with tool use for iterative testing.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
SubAgentResult with analysis text and token counts.
|
|
411
|
+
"""
|
|
412
|
+
user_content = f"""Debug the hook with ID {hook_id} using annotation ID {annotation_id}.
|
|
413
|
+
|
|
414
|
+
Steps:
|
|
415
|
+
1. Call `get_hook` with hook_id="{hook_id}" to fetch the hook code (in config.code)
|
|
416
|
+
2. Call `get_annotation` with annotation_id="{annotation_id}" to fetch the annotation data
|
|
417
|
+
{f'3. Optionally call `get_schema` with schema_id="{schema_id}" if needed' if schema_id else ""}
|
|
418
|
+
3. Use `evaluate_python_hook` to execute the code and debug any issues
|
|
419
|
+
4. Fix and verify your fixes work before providing your final answer"""
|
|
420
|
+
|
|
421
|
+
sub_agent = HookDebugSubAgent()
|
|
422
|
+
return sub_agent.run(user_content)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@beta_tool
|
|
426
|
+
def evaluate_python_hook(
|
|
427
|
+
code: str,
|
|
428
|
+
annotation_json: str,
|
|
429
|
+
schema_json: str | None = None,
|
|
430
|
+
) -> str:
|
|
431
|
+
"""Execute Rossum function hook Python code against test annotation/schema data for debugging.
|
|
432
|
+
|
|
433
|
+
This runs the provided code in a restricted environment, looks for a function named
|
|
434
|
+
`rossum_hook_request_handler`, and calls it with a payload containing the annotation
|
|
435
|
+
and optional schema data.
|
|
436
|
+
|
|
437
|
+
**IMPORTANT**: This is for debugging only. No imports or external I/O are allowed.
|
|
438
|
+
The code runs in a sandboxed environment with limited builtins.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
code: Full Python source containing a function:
|
|
442
|
+
`def rossum_hook_request_handler(payload): ...`
|
|
443
|
+
The function receives a dict with 'annotation' and optionally 'schema' keys.
|
|
444
|
+
annotation_json: JSON string of the annotation object as seen in hook payload["annotation"].
|
|
445
|
+
Get this from the get_annotation MCP tool.
|
|
446
|
+
schema_json: Optional JSON string of the schema object as seen in payload["schema"].
|
|
447
|
+
Get this from the get_schema MCP tool.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
A JSON string with structure:
|
|
451
|
+
{
|
|
452
|
+
"status": "success" | "error" | "invalid_input",
|
|
453
|
+
"result": <return value from handler, if any>,
|
|
454
|
+
"stdout": "<captured stdout from print statements>",
|
|
455
|
+
"stderr": "<captured stderr>",
|
|
456
|
+
"exception": {"type": "...", "message": "...", "traceback": "..."} | null,
|
|
457
|
+
"elapsed_ms": <execution time in milliseconds>
|
|
458
|
+
}
|
|
459
|
+
"""
|
|
460
|
+
start_time = time.perf_counter()
|
|
461
|
+
|
|
462
|
+
if not code:
|
|
463
|
+
return _make_evaluate_response("invalid_input", start_time, stderr="No code provided")
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
annotation = json.loads(annotation_json)
|
|
467
|
+
except Exception as e:
|
|
468
|
+
logger.exception("Failed to parse annotation_json")
|
|
469
|
+
return _make_evaluate_response("invalid_input", start_time, stderr=f"Invalid annotation_json: {e}", exc=e)
|
|
470
|
+
|
|
471
|
+
schema = None
|
|
472
|
+
if schema_json:
|
|
473
|
+
try:
|
|
474
|
+
schema = json.loads(schema_json)
|
|
475
|
+
except Exception as e:
|
|
476
|
+
logger.exception("Failed to parse schema_json")
|
|
477
|
+
return _make_evaluate_response("invalid_input", start_time, stderr=f"Invalid schema_json: {e}", exc=e)
|
|
478
|
+
|
|
479
|
+
payload: dict[str, Any] = {"annotation": annotation}
|
|
480
|
+
if schema is not None:
|
|
481
|
+
payload["schema"] = schema
|
|
482
|
+
|
|
483
|
+
safe_builtins: dict[str, object] = {
|
|
484
|
+
name: getattr(builtins, name) for name in _ALLOWED_BUILTIN_NAMES if hasattr(builtins, name)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
exec_namespace: dict[str, Any] = {
|
|
488
|
+
"__builtins__": safe_builtins,
|
|
489
|
+
"collections": collections,
|
|
490
|
+
"datetime": dt,
|
|
491
|
+
"decimal": decimal,
|
|
492
|
+
"Decimal": Decimal,
|
|
493
|
+
"InvalidOperation": InvalidOperation,
|
|
494
|
+
"functools": functools,
|
|
495
|
+
"itertools": itertools,
|
|
496
|
+
"json": json,
|
|
497
|
+
"math": math,
|
|
498
|
+
"re": re,
|
|
499
|
+
"string": string,
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
stdout_buf = io.StringIO()
|
|
503
|
+
stderr_buf = io.StringIO()
|
|
504
|
+
|
|
505
|
+
clean_code = _strip_imports(code)
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
|
509
|
+
exec(clean_code, exec_namespace)
|
|
510
|
+
|
|
511
|
+
handler = exec_namespace.get("rossum_hook_request_handler")
|
|
512
|
+
if handler is None or not callable(handler):
|
|
513
|
+
raise RuntimeError(
|
|
514
|
+
"No callable `rossum_hook_request_handler` found. "
|
|
515
|
+
"Define it as `def rossum_hook_request_handler(payload): ...`"
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
result = handler(payload)
|
|
519
|
+
|
|
520
|
+
return _make_evaluate_response(
|
|
521
|
+
status="success",
|
|
522
|
+
start_time=start_time,
|
|
523
|
+
result=result,
|
|
524
|
+
stdout=stdout_buf.getvalue(),
|
|
525
|
+
stderr=stderr_buf.getvalue(),
|
|
526
|
+
)
|
|
527
|
+
except Exception as e:
|
|
528
|
+
logger.exception("Error while evaluating python hook")
|
|
529
|
+
return _make_evaluate_response(
|
|
530
|
+
status="error",
|
|
531
|
+
start_time=start_time,
|
|
532
|
+
result=None,
|
|
533
|
+
stdout=stdout_buf.getvalue(),
|
|
534
|
+
stderr=stderr_buf.getvalue(),
|
|
535
|
+
exc=e,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@beta_tool
|
|
540
|
+
def debug_hook(hook_id: str, annotation_id: str, schema_id: str | None = None) -> str:
|
|
541
|
+
"""Debug a Rossum hook using an Opus sub-agent. ALWAYS use this tool when debugging hook code errors.
|
|
542
|
+
|
|
543
|
+
This is the PRIMARY tool for debugging Python function hooks. Simply pass the hook ID and annotation ID,
|
|
544
|
+
and the Opus sub-agent will fetch the data and debug the hook.
|
|
545
|
+
|
|
546
|
+
The tool:
|
|
547
|
+
1. Fetches hook code and annotation data via MCP tools
|
|
548
|
+
2. Executes and analyzes errors with Claude Opus for deep reasoning
|
|
549
|
+
3. Iteratively fixes and verifies the code works
|
|
550
|
+
4. Returns detailed analysis with working code
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
hook_id: The hook ID (from get_hook or hook URL). The sub-agent will fetch the code.
|
|
554
|
+
annotation_id: The annotation ID to use for testing. The sub-agent will fetch the data.
|
|
555
|
+
schema_id: Optional schema ID if schema context is needed.
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
JSON with Opus expert analysis including fixed code and token usage.
|
|
559
|
+
"""
|
|
560
|
+
start_time = time.perf_counter()
|
|
561
|
+
|
|
562
|
+
if not hook_id:
|
|
563
|
+
return json.dumps(
|
|
564
|
+
{"error": "No hook_id provided", "elapsed_ms": round((time.perf_counter() - start_time) * 1000, 3)}
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if not annotation_id:
|
|
568
|
+
return json.dumps(
|
|
569
|
+
{"error": "No annotation_id provided", "elapsed_ms": round((time.perf_counter() - start_time) * 1000, 3)}
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
logger.info(f"debug_hook: Calling Opus sub-agent for hook_id={hook_id}, annotation_id={annotation_id}")
|
|
573
|
+
result = _call_opus_for_debug(hook_id, annotation_id, schema_id)
|
|
574
|
+
elapsed_ms = round((time.perf_counter() - start_time) * 1000, 3)
|
|
575
|
+
|
|
576
|
+
logger.info(
|
|
577
|
+
f"debug_hook: completed in {elapsed_ms:.1f}ms, "
|
|
578
|
+
f"tokens in={result.input_tokens} out={result.output_tokens}, "
|
|
579
|
+
f"iterations={result.iterations_used}"
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
response = {
|
|
583
|
+
"hook_id": hook_id,
|
|
584
|
+
"annotation_id": annotation_id,
|
|
585
|
+
"analysis": result.analysis,
|
|
586
|
+
"elapsed_ms": elapsed_ms,
|
|
587
|
+
"input_tokens": result.input_tokens,
|
|
588
|
+
"output_tokens": result.output_tokens,
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return json.dumps(response, ensure_ascii=False, default=str)
|