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,176 @@
1
+ """Memory management for the agent.
2
+
3
+ This module implements the memory storage system following the smolagents pattern:
4
+ - Store structured MemoryStep objects (not raw messages)
5
+ - Rebuild messages fresh each call via write_to_messages()
6
+ - Apply summary_mode for old steps to reduce token usage
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from anthropic.types import MessageParam, TextBlockParam, ThinkingBlockParam, ToolResultBlockParam, ToolUseBlockParam
15
+
16
+ from rossum_agent.agent.models import ThinkingBlockData, ToolCall, ToolResult
17
+
18
+ if TYPE_CHECKING:
19
+ from rossum_agent.agent.types import UserContent
20
+
21
+
22
+ @dataclass
23
+ class MemoryStep:
24
+ """A single step stored in agent memory.
25
+
26
+ This is the structured storage format. Steps are converted to messages
27
+ on-the-fly via to_messages(), allowing summary_mode to compress old steps.
28
+
29
+ Attributes:
30
+ text: Model's text output (reasoning before tool calls, or final answer).
31
+ """
32
+
33
+ step_number: int
34
+ text: str | None = None
35
+ tool_calls: list[ToolCall] = field(default_factory=list)
36
+ tool_results: list[ToolResult] = field(default_factory=list)
37
+ thinking_blocks: list[ThinkingBlockData] = field(default_factory=list)
38
+ input_tokens: int = 0
39
+ output_tokens: int = 0
40
+
41
+ def to_messages(self) -> list[MessageParam]:
42
+ """Convert this step to Anthropic message format.
43
+
44
+ For tool-use steps: Includes text block followed by tool_use blocks.
45
+ For final answer steps: Includes text as assistant content.
46
+
47
+ Returns:
48
+ List of message dicts for the Anthropic API.
49
+ """
50
+ messages: list[MessageParam] = []
51
+
52
+ if self.tool_calls:
53
+ assistant_content: list[TextBlockParam | ToolUseBlockParam | ThinkingBlockParam] = [
54
+ tb.to_dict() for tb in self.thinking_blocks
55
+ ]
56
+
57
+ if self.text:
58
+ assistant_content.append(TextBlockParam(type="text", text=self.text))
59
+
60
+ assistant_content.extend(
61
+ ToolUseBlockParam(type="tool_use", id=tc.id, name=tc.name, input=tc.arguments)
62
+ for tc in self.tool_calls
63
+ )
64
+
65
+ messages.append(MessageParam(role="assistant", content=assistant_content))
66
+
67
+ if self.tool_results:
68
+ tool_result_blocks = [
69
+ ToolResultBlockParam(
70
+ type="tool_result",
71
+ tool_use_id=tr.tool_call_id,
72
+ content=tr.content,
73
+ is_error=tr.is_error,
74
+ )
75
+ for tr in self.tool_results
76
+ ]
77
+ messages.append(MessageParam(role="user", content=tool_result_blocks))
78
+
79
+ elif self.text:
80
+ messages.append(MessageParam(role="assistant", content=self.text))
81
+
82
+ return messages
83
+
84
+ def to_dict(self) -> dict[str, Any]:
85
+ """Serialize to dictionary for storage."""
86
+ return {
87
+ "type": "memory_step",
88
+ "step_number": self.step_number,
89
+ "text": self.text,
90
+ "tool_calls": [tc.to_dict() for tc in self.tool_calls],
91
+ "tool_results": [tr.to_dict() for tr in self.tool_results],
92
+ "thinking_blocks": [tb.to_dict() for tb in self.thinking_blocks],
93
+ "input_tokens": self.input_tokens,
94
+ "output_tokens": self.output_tokens,
95
+ }
96
+
97
+ @classmethod
98
+ def from_dict(cls, data: dict[str, Any]) -> MemoryStep:
99
+ """Deserialize from dictionary."""
100
+ return cls(
101
+ step_number=data.get("step_number", 0),
102
+ text=data.get("text"),
103
+ tool_calls=[ToolCall.from_dict(tc) for tc in data.get("tool_calls", [])],
104
+ tool_results=[ToolResult.from_dict(tr) for tr in data.get("tool_results", [])],
105
+ thinking_blocks=[ThinkingBlockData.from_dict(tb) for tb in data.get("thinking_blocks", [])],
106
+ input_tokens=data.get("input_tokens", 0),
107
+ output_tokens=data.get("output_tokens", 0),
108
+ )
109
+
110
+
111
+ @dataclass
112
+ class TaskStep:
113
+ """Represents the initial user task/prompt.
114
+
115
+ Supports both text-only and multimodal content (with images).
116
+ """
117
+
118
+ task: UserContent
119
+
120
+ def to_messages(self) -> list[MessageParam]:
121
+ return [MessageParam(role="user", content=self.task)]
122
+
123
+ def to_dict(self) -> dict[str, Any]:
124
+ """Serialize to dictionary for storage."""
125
+ return {"type": "task_step", "task": self.task}
126
+
127
+ @classmethod
128
+ def from_dict(cls, data: dict[str, Any]) -> TaskStep:
129
+ """Deserialize from dictionary."""
130
+ return cls(task=data["task"])
131
+
132
+
133
+ @dataclass
134
+ class AgentMemory:
135
+ """Memory storage for agent steps.
136
+
137
+ Stores structured step objects and rebuilds messages on demand.
138
+ """
139
+
140
+ steps: list[TaskStep | MemoryStep] = field(default_factory=list)
141
+
142
+ def reset(self) -> None:
143
+ """Clear all steps."""
144
+ self.steps = []
145
+
146
+ def add_task(self, task: UserContent) -> None:
147
+ """Add initial user task (text or multimodal content)."""
148
+ self.steps.append(TaskStep(task=task))
149
+
150
+ def add_step(self, step: MemoryStep) -> None:
151
+ """Add a completed agent step."""
152
+ self.steps.append(step)
153
+
154
+ def write_to_messages(self) -> list[MessageParam]:
155
+ """Convert all steps to messages.
156
+
157
+ Returns:
158
+ List of message dicts ready for Anthropic API.
159
+ """
160
+ return [msg for step in self.steps for msg in step.to_messages()]
161
+
162
+ def to_dict(self) -> list[dict[str, Any]]:
163
+ """Serialize all steps to a list of dictionaries for storage."""
164
+ return [step.to_dict() for step in self.steps]
165
+
166
+ @classmethod
167
+ def from_dict(cls, data: list[dict[str, Any]]) -> AgentMemory:
168
+ """Deserialize from a list of step dictionaries."""
169
+ memory = cls()
170
+ for step_data in data:
171
+ step_type = step_data.get("type")
172
+ if step_type == "task_step":
173
+ memory.steps.append(TaskStep.from_dict(step_data))
174
+ elif step_type == "memory_step":
175
+ memory.steps.append(MemoryStep.from_dict(step_data))
176
+ return memory
@@ -0,0 +1,160 @@
1
+ """Data models for the agent module.
2
+
3
+ This module contains the core data classes used throughout the agent system
4
+ for representing tool calls, results, and agent steps.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from typing import TYPE_CHECKING, Any, Literal
12
+
13
+ from anthropic.types import ThinkingBlockParam
14
+
15
+
16
+ class StepType(Enum):
17
+ """Type of streaming step for distinguishing UI rendering."""
18
+
19
+ THINKING = "thinking"
20
+ INTERMEDIATE = "intermediate"
21
+ FINAL_ANSWER = "final_answer"
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from rossum_agent.tools import SubAgentProgress
26
+
27
+
28
+ @dataclass
29
+ class ToolCall:
30
+ """Represents a single tool call made by the agent."""
31
+
32
+ id: str
33
+ name: str
34
+ arguments: dict[str, Any]
35
+
36
+ def to_dict(self) -> dict[str, Any]:
37
+ """Serialize to dictionary for storage."""
38
+ return {"id": self.id, "name": self.name, "arguments": self.arguments}
39
+
40
+ @classmethod
41
+ def from_dict(cls, data: dict[str, Any]) -> ToolCall:
42
+ """Deserialize from dictionary."""
43
+ return cls(id=data["id"], name=data["name"], arguments=data.get("arguments", {}))
44
+
45
+
46
+ @dataclass
47
+ class ToolResult:
48
+ """Represents the result of a tool call."""
49
+
50
+ tool_call_id: str
51
+ name: str
52
+ content: str
53
+ is_error: bool = False
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ """Serialize to dictionary for storage."""
57
+ return {
58
+ "tool_call_id": self.tool_call_id,
59
+ "name": self.name,
60
+ "content": self.content,
61
+ "is_error": self.is_error,
62
+ }
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: dict[str, Any]) -> ToolResult:
66
+ """Deserialize from dictionary."""
67
+ return cls(
68
+ tool_call_id=data["tool_call_id"],
69
+ name=data["name"],
70
+ content=data.get("content", ""),
71
+ is_error=data.get("is_error", False),
72
+ )
73
+
74
+
75
+ @dataclass
76
+ class StreamDelta:
77
+ """A tagged delta from stream processing - either thinking or text."""
78
+
79
+ kind: Literal["thinking", "text"]
80
+ content: str
81
+
82
+
83
+ @dataclass
84
+ class ThinkingBlockData:
85
+ """Represents a thinking block from extended thinking.
86
+
87
+ Must be preserved and passed back to the API when continuing tool use conversations.
88
+ """
89
+
90
+ thinking: str
91
+ signature: str
92
+
93
+ def to_dict(self) -> ThinkingBlockParam:
94
+ """Serialize to dictionary for storage and API message format."""
95
+ return ThinkingBlockParam(type="thinking", thinking=self.thinking, signature=self.signature)
96
+
97
+ @classmethod
98
+ def from_dict(cls, data: dict[str, Any]) -> ThinkingBlockData:
99
+ """Deserialize from dictionary."""
100
+ return cls(thinking=data["thinking"], signature=data["signature"])
101
+
102
+
103
+ @dataclass
104
+ class AgentStep:
105
+ """Represents a single step in the agent's execution (for yielding to caller).
106
+
107
+ This is the public-facing step object yielded during agent.run().
108
+ Different from MemoryStep which is for internal storage.
109
+ """
110
+
111
+ step_number: int
112
+ thinking: str | None = None
113
+ tool_calls: list[ToolCall] = field(default_factory=list)
114
+ tool_results: list[ToolResult] = field(default_factory=list)
115
+ final_answer: str | None = None
116
+ is_final: bool = False
117
+ error: str | None = None
118
+ is_streaming: bool = False
119
+ input_tokens: int = 0
120
+ output_tokens: int = 0
121
+ current_tool: str | None = None
122
+ tool_progress: tuple[int, int] | None = None
123
+ sub_agent_progress: SubAgentProgress | None = None
124
+ text_delta: str | None = None
125
+ accumulated_text: str | None = None
126
+ step_type: StepType | None = None
127
+
128
+ def has_tool_calls(self) -> bool:
129
+ """Check if this step contains tool calls."""
130
+ return bool(self.tool_calls)
131
+
132
+
133
+ @dataclass
134
+ class AgentConfig:
135
+ """Configuration for the RossumAgent."""
136
+
137
+ max_output_tokens: int = 64000 # Opus 4.5 limit
138
+ max_steps: int = 50
139
+ temperature: float = 1.0 # Required for extended thinking
140
+ request_delay: float = 3.0 # Delay in seconds between API calls to avoid rate limiting
141
+ thinking_budget_tokens: int = 10000 # Budget for extended thinking (min 1024)
142
+
143
+ def __post_init__(self) -> None:
144
+ if self.temperature != 1.0:
145
+ msg = "temperature must be 1.0 when extended thinking is enabled"
146
+ raise ValueError(msg)
147
+ if self.thinking_budget_tokens < 1024:
148
+ msg = "thinking_budget_tokens must be at least 1024"
149
+ raise ValueError(msg)
150
+
151
+
152
+ MAX_TOOL_OUTPUT_LENGTH = 20000
153
+
154
+
155
+ def truncate_content(content: str, max_length: int = MAX_TOOL_OUTPUT_LENGTH) -> str:
156
+ """Truncate content preserving head and tail."""
157
+ if len(content) <= max_length:
158
+ return content
159
+ half = max_length // 2
160
+ return content[:half] + f"\n..._Content truncated to stay below {max_length} characters_...\n" + content[-half:]
@@ -0,0 +1,152 @@
1
+ """Lightweight request classifier for filtering out-of-scope requests.
2
+
3
+ This module provides a fast pre-filter that checks if a user request is within
4
+ the scope of the Rossum platform assistant before engaging the full agent.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+ from typing import TYPE_CHECKING
13
+
14
+ from rossum_agent.bedrock_client import get_small_model_id
15
+
16
+ if TYPE_CHECKING:
17
+ from anthropic import AnthropicBedrock
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ CLASSIFIER_PROMPT = """You are a scope classifier for a Rossum document processing platform assistant.
22
+
23
+ The assistant can help with:
24
+ - Queue, hook, schema, and extension analysis/configuration
25
+ - Debugging document processing issues and errors
26
+ - Investigating hook logs and extension behavior
27
+ - Explaining workflows and automation
28
+ - Writing analysis reports about Rossum configuration issues
29
+
30
+ IN_SCOPE: Request relates to Rossum PLATFORM operations
31
+ - setting up new organization
32
+ - analyzing/configuring queues, hooks, schemas, extensions
33
+ - debugging errors, investigating logs
34
+ - explaining workflows, analysis
35
+ - generating structured report of customer use-cases on the platform
36
+ - generating formula fields suggestions
37
+ - Also: user asks what the assistant can do, greets assistant
38
+
39
+ OUT_OF_SCOPE: Request is for DATA analytics - aggregating extracted data, generating charts/plots from document data, summarizing line items/amounts across documents, creating files unrelated to Rossum debugging. Even if it mentions Rossum annotations, if the goal is data aggregation/visualization, it's OUT_OF_SCOPE.
40
+
41
+ Examples:
42
+ - "Investigate errors with document splitting on queue X" → IN_SCOPE (debugging)
43
+ - "Aggregate line item amounts and generate a bar chart" → OUT_OF_SCOPE (data analytics)
44
+ - "Create a markdown saying hello" → OUT_OF_SCOPE (generic file creation)
45
+
46
+ Respond with exactly one word: IN_SCOPE or OUT_OF_SCOPE
47
+
48
+ User request: {message}"""
49
+
50
+ CLASSIFIER_MAX_TOKENS = 10
51
+
52
+
53
+ class RequestScope(Enum):
54
+ """Classification result for a user request."""
55
+
56
+ IN_SCOPE = "IN_SCOPE"
57
+ OUT_OF_SCOPE = "OUT_OF_SCOPE"
58
+
59
+
60
+ @dataclass
61
+ class ClassificationResult:
62
+ """Result of request classification."""
63
+
64
+ scope: RequestScope
65
+ raw_response: str
66
+ input_tokens: int = 0
67
+ output_tokens: int = 0
68
+
69
+
70
+ @dataclass
71
+ class RejectionResult:
72
+ """Result of rejection response generation."""
73
+
74
+ response: str
75
+ input_tokens: int = 0
76
+ output_tokens: int = 0
77
+
78
+
79
+ REJECTION_PROMPT = """You are an expert Rossum platform specialist. The user made a request that is outside your scope.
80
+
81
+ I can help with:
82
+ - Analyzing and debugging hooks, extensions, and workflows
83
+ - Documenting queue configurations
84
+ - Investigating processing errors
85
+ - Configuring automation
86
+
87
+ The user asked: {message}
88
+
89
+ Write a brief, helpful response that:
90
+ 1. Politely explains this is outside your Rossum platform expertise
91
+ 2. Briefly mentions 2-3 relevant things you CAN help with from the capabilities above
92
+ 3. Asks if they have any Rossum-related questions
93
+
94
+ Keep it concise (3-4 sentences max). Be friendly, not robotic."""
95
+
96
+ REJECTION_MAX_TOKENS = 300
97
+
98
+
99
+ def generate_rejection_response(client: AnthropicBedrock, message: str) -> RejectionResult:
100
+ """Generate a contextual rejection response for out-of-scope requests."""
101
+ prompt = REJECTION_PROMPT.format(message=message)
102
+ try:
103
+ response = client.messages.create(
104
+ model=get_small_model_id(), max_tokens=REJECTION_MAX_TOKENS, messages=[{"role": "user", "content": prompt}]
105
+ )
106
+ text = response.content[0].text.strip() if response.content else _fallback_response()
107
+ return RejectionResult(
108
+ response=text, input_tokens=response.usage.input_tokens, output_tokens=response.usage.output_tokens
109
+ )
110
+ except Exception as e:
111
+ logger.warning(f"Rejection response generation failed: {e}")
112
+ return RejectionResult(response=_fallback_response())
113
+
114
+
115
+ def _fallback_response() -> str:
116
+ return (
117
+ "I'm an expert Rossum platform specialist focused on document processing workflows. "
118
+ "Your request appears to be outside my area of expertise. "
119
+ "I can help with analyzing hooks, debugging extensions, documenting queue configurations, "
120
+ "and configuring automation workflows. Do you have any Rossum-related questions?"
121
+ )
122
+
123
+
124
+ def classify_request(client: AnthropicBedrock, message: str) -> ClassificationResult:
125
+ """Classify whether a user request is within scope.
126
+
127
+ Uses a fast, cheap model (Haiku) with minimal tokens to quickly determine if the request should be processed by the main agent.
128
+ """
129
+ prompt = CLASSIFIER_PROMPT.format(message=message)
130
+
131
+ try:
132
+ response = client.messages.create(
133
+ model=get_small_model_id(),
134
+ max_tokens=CLASSIFIER_MAX_TOKENS,
135
+ messages=[{"role": "user", "content": prompt}],
136
+ )
137
+
138
+ raw_response = response.content[0].text.strip().upper() if response.content else ""
139
+
140
+ scope = RequestScope.OUT_OF_SCOPE if "OUT_OF_SCOPE" in raw_response else RequestScope.IN_SCOPE
141
+
142
+ logger.debug(f"Request classified as {scope.value}: {message[:50]}...")
143
+ return ClassificationResult(
144
+ scope=scope,
145
+ raw_response=raw_response,
146
+ input_tokens=response.usage.input_tokens,
147
+ output_tokens=response.usage.output_tokens,
148
+ )
149
+
150
+ except Exception as e:
151
+ logger.warning(f"Classification failed, defaulting to IN_SCOPE: {e}")
152
+ return ClassificationResult(scope=RequestScope.IN_SCOPE, raw_response=f"error: {e}")
@@ -0,0 +1,132 @@
1
+ """Skills module for loading and managing agent skills.
2
+
3
+ Skills are markdown files that provide domain-specific instructions and workflow to the agent. They are loaded
4
+ from the skills directory and injected into the agent's system prompt.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Default skills directory path relative to rossum_agent package
16
+ _SKILLS_DIR = Path(__file__).parent.parent / "skills"
17
+
18
+
19
+ @dataclass
20
+ class Skill:
21
+ """Represents a loaded skill with its content and metadata."""
22
+
23
+ name: str
24
+ content: str
25
+ file_path: Path
26
+
27
+ @property
28
+ def slug(self) -> str:
29
+ """Get the skill slug (filename without extension)."""
30
+ return self.file_path.stem
31
+
32
+
33
+ class SkillRegistry:
34
+ """Registry for loading and managing agent skills.
35
+
36
+ Skills are markdown files in the skills directory that provide domain-specific instructions for the agent.
37
+ """
38
+
39
+ def __init__(self, skills_dir: Path | None = None) -> None:
40
+ self.skills_dir = skills_dir or _SKILLS_DIR
41
+ self._skills: dict[str, Skill] = {}
42
+ self._loaded = False
43
+
44
+ def _load_skills(self) -> None:
45
+ """Load all skills from the skills directory."""
46
+ if self._loaded:
47
+ return
48
+
49
+ if not self.skills_dir.exists():
50
+ logger.warning(f"Skills directory not found: {self.skills_dir}")
51
+ self._loaded = True
52
+ return
53
+
54
+ for skill_file in self.skills_dir.glob("*.md"):
55
+ try:
56
+ content = skill_file.read_text(encoding="utf-8")
57
+ skill = Skill(
58
+ name=skill_file.stem.replace("-", " ").replace("_", " ").title(),
59
+ content=content,
60
+ file_path=skill_file,
61
+ )
62
+ self._skills[skill.slug] = skill
63
+ logger.debug(f"Loaded skill: {skill.name} from {skill_file}")
64
+ except Exception as e:
65
+ logger.error(f"Failed to load skill from {skill_file}: {e}")
66
+
67
+ self._loaded = True
68
+ logger.info(f"Loaded {len(self._skills)} skills from {self.skills_dir}")
69
+
70
+ def get_skill(self, slug: str) -> Skill | None:
71
+ """Get a skill by its slug (filename without extension).
72
+
73
+ Args:
74
+ slug: The skill slug (e.g., "rossum-deployment").
75
+
76
+ Returns:
77
+ The Skill object if found, None otherwise.
78
+ """
79
+ self._load_skills()
80
+ return self._skills.get(slug)
81
+
82
+ def get_all_skills(self) -> list[Skill]:
83
+ self._load_skills()
84
+ return list(self._skills.values())
85
+
86
+ def get_skill_names(self) -> list[str]:
87
+ self._load_skills()
88
+ return list(self._skills.keys())
89
+
90
+ def reload(self) -> None:
91
+ """Force reload all skills from disk."""
92
+ self._skills.clear()
93
+ self._loaded = False
94
+ self._load_skills()
95
+
96
+
97
+ # Module-level default registry instance
98
+ _default_registry: SkillRegistry | None = None
99
+
100
+
101
+ def get_skill_registry(skills_dir: Path | None = None) -> SkillRegistry:
102
+ global _default_registry
103
+ if _default_registry is None or skills_dir is not None:
104
+ _default_registry = SkillRegistry(skills_dir)
105
+ return _default_registry
106
+
107
+
108
+ def get_skill(slug: str) -> Skill | None:
109
+ return get_skill_registry().get_skill(slug)
110
+
111
+
112
+ def get_all_skills() -> list[Skill]:
113
+ return get_skill_registry().get_all_skills()
114
+
115
+
116
+ def format_skills_for_prompt(skills: list[Skill] | None = None) -> str:
117
+ if skills is None:
118
+ skills = get_all_skills()
119
+
120
+ if not skills:
121
+ return ""
122
+
123
+ sections = []
124
+ for skill in skills:
125
+ sections.append(f"\n{'=' * 60}\n{skill.content}\n{'=' * 60}")
126
+
127
+ return "\n\n## Available Skills\n" + "\n".join(sections)
128
+
129
+
130
+ def get_skill_content(slug: str) -> str | None:
131
+ skill = get_skill(slug)
132
+ return skill.content if skill else None
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from anthropic.types import ImageBlockParam, TextBlockParam
4
+
5
+ UserContent = str | list[TextBlockParam | ImageBlockParam]
@@ -0,0 +1,56 @@
1
+ """Agent logging wrapper for tracking all agent calls and tool usage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from rossum_agent.agent import AgentStep
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def log_agent_result(
16
+ result: AgentStep, prompt: str = "", duration: float = 0, total_input_tokens: int = 0, total_output_tokens: int = 0
17
+ ) -> None:
18
+ """Log agent execution result.
19
+
20
+ Args:
21
+ result: The AgentStep to log.
22
+ prompt: The original user prompt.
23
+ duration: Time taken for the step in seconds.
24
+ total_input_tokens: Total input tokens across all steps.
25
+ total_output_tokens: Total output tokens across all steps.
26
+ """
27
+ extra_fields: dict[str, object] = {
28
+ "event_type": "agent_call_complete",
29
+ "prompt": prompt,
30
+ "duration_seconds": duration,
31
+ "step_number": result.step_number,
32
+ "is_final": result.is_final,
33
+ "input_tokens": total_input_tokens if total_input_tokens else result.input_tokens,
34
+ "output_tokens": total_output_tokens if total_output_tokens else result.output_tokens,
35
+ }
36
+
37
+ if result.thinking:
38
+ extra_fields["thinking"] = result.thinking[:500]
39
+
40
+ if result.tool_calls:
41
+ extra_fields["tool_calls"] = json.dumps(
42
+ [{"name": tc.name, "arguments": tc.arguments} for tc in result.tool_calls], default=str
43
+ )
44
+
45
+ if result.tool_results:
46
+ extra_fields["tool_results"] = json.dumps(
47
+ [{"name": tr.name, "is_error": tr.is_error} for tr in result.tool_results], default=str
48
+ )
49
+
50
+ if result.final_answer:
51
+ extra_fields["final_answer"] = result.final_answer[:500]
52
+
53
+ if result.error:
54
+ extra_fields["error"] = result.error
55
+
56
+ logger.info("Agent step completed", extra=extra_fields)
@@ -0,0 +1 @@
1
+ """FastAPI-based REST API for Rossum Agent."""