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,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,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."""
|