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.
Files changed (67) hide show
  1. rossum_agent/__init__.py +9 -0
  2. rossum_agent/agent/__init__.py +32 -0
  3. rossum_agent/agent/core.py +932 -0
  4. rossum_agent/agent/memory.py +176 -0
  5. rossum_agent/agent/models.py +160 -0
  6. rossum_agent/agent/request_classifier.py +152 -0
  7. rossum_agent/agent/skills.py +132 -0
  8. rossum_agent/agent/types.py +5 -0
  9. rossum_agent/agent_logging.py +56 -0
  10. rossum_agent/api/__init__.py +1 -0
  11. rossum_agent/api/cli.py +51 -0
  12. rossum_agent/api/dependencies.py +190 -0
  13. rossum_agent/api/main.py +180 -0
  14. rossum_agent/api/models/__init__.py +1 -0
  15. rossum_agent/api/models/schemas.py +301 -0
  16. rossum_agent/api/routes/__init__.py +1 -0
  17. rossum_agent/api/routes/chats.py +95 -0
  18. rossum_agent/api/routes/files.py +113 -0
  19. rossum_agent/api/routes/health.py +44 -0
  20. rossum_agent/api/routes/messages.py +218 -0
  21. rossum_agent/api/services/__init__.py +1 -0
  22. rossum_agent/api/services/agent_service.py +451 -0
  23. rossum_agent/api/services/chat_service.py +197 -0
  24. rossum_agent/api/services/file_service.py +65 -0
  25. rossum_agent/assets/Primary_light_logo.png +0 -0
  26. rossum_agent/bedrock_client.py +64 -0
  27. rossum_agent/prompts/__init__.py +27 -0
  28. rossum_agent/prompts/base_prompt.py +80 -0
  29. rossum_agent/prompts/system_prompt.py +24 -0
  30. rossum_agent/py.typed +0 -0
  31. rossum_agent/redis_storage.py +482 -0
  32. rossum_agent/rossum_mcp_integration.py +123 -0
  33. rossum_agent/skills/hook-debugging.md +31 -0
  34. rossum_agent/skills/organization-setup.md +60 -0
  35. rossum_agent/skills/rossum-deployment.md +102 -0
  36. rossum_agent/skills/schema-patching.md +61 -0
  37. rossum_agent/skills/schema-pruning.md +23 -0
  38. rossum_agent/skills/ui-settings.md +45 -0
  39. rossum_agent/streamlit_app/__init__.py +1 -0
  40. rossum_agent/streamlit_app/app.py +646 -0
  41. rossum_agent/streamlit_app/beep_sound.py +36 -0
  42. rossum_agent/streamlit_app/cli.py +17 -0
  43. rossum_agent/streamlit_app/render_modules.py +123 -0
  44. rossum_agent/streamlit_app/response_formatting.py +305 -0
  45. rossum_agent/tools/__init__.py +214 -0
  46. rossum_agent/tools/core.py +173 -0
  47. rossum_agent/tools/deploy.py +404 -0
  48. rossum_agent/tools/dynamic_tools.py +365 -0
  49. rossum_agent/tools/file_tools.py +62 -0
  50. rossum_agent/tools/formula.py +187 -0
  51. rossum_agent/tools/skills.py +31 -0
  52. rossum_agent/tools/spawn_mcp.py +227 -0
  53. rossum_agent/tools/subagents/__init__.py +31 -0
  54. rossum_agent/tools/subagents/base.py +303 -0
  55. rossum_agent/tools/subagents/hook_debug.py +591 -0
  56. rossum_agent/tools/subagents/knowledge_base.py +305 -0
  57. rossum_agent/tools/subagents/mcp_helpers.py +47 -0
  58. rossum_agent/tools/subagents/schema_patching.py +471 -0
  59. rossum_agent/url_context.py +167 -0
  60. rossum_agent/user_detection.py +100 -0
  61. rossum_agent/utils.py +128 -0
  62. rossum_agent-1.0.0rc0.dist-info/METADATA +311 -0
  63. rossum_agent-1.0.0rc0.dist-info/RECORD +67 -0
  64. rossum_agent-1.0.0rc0.dist-info/WHEEL +5 -0
  65. rossum_agent-1.0.0rc0.dist-info/entry_points.txt +3 -0
  66. rossum_agent-1.0.0rc0.dist-info/licenses/LICENSE +21 -0
  67. 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)