rossum-agent 1.0.0rc3__tar.gz → 1.0.0rc5__tar.gz

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 (83) hide show
  1. {rossum_agent-1.0.0rc3/rossum_agent.egg-info → rossum_agent-1.0.0rc5}/PKG-INFO +6 -1
  2. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/README.md +5 -0
  3. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/pyproject.toml +1 -1
  4. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/__init__.py +1 -1
  5. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/agent/core.py +0 -31
  6. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/models/schemas.py +4 -0
  7. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/routes/messages.py +5 -0
  8. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/services/agent_service.py +53 -25
  9. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/prompts/base_prompt.py +1 -1
  10. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5/rossum_agent.egg-info}/PKG-INFO +6 -1
  11. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent.egg-info/SOURCES.txt +0 -1
  12. rossum_agent-1.0.0rc3/rossum_agent/agent/request_classifier.py +0 -152
  13. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/LICENSE +0 -0
  14. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/agent/__init__.py +0 -0
  15. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/agent/memory.py +0 -0
  16. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/agent/models.py +0 -0
  17. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/agent/skills.py +0 -0
  18. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/agent/types.py +0 -0
  19. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/agent_logging.py +0 -0
  20. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/__init__.py +0 -0
  21. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/cli.py +0 -0
  22. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/dependencies.py +0 -0
  23. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/main.py +0 -0
  24. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/models/__init__.py +0 -0
  25. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/routes/__init__.py +0 -0
  26. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/routes/chats.py +0 -0
  27. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/routes/files.py +0 -0
  28. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/routes/health.py +0 -0
  29. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/services/__init__.py +0 -0
  30. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/services/chat_service.py +0 -0
  31. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/api/services/file_service.py +0 -0
  32. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/assets/Primary_light_logo.png +0 -0
  33. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/bedrock_client.py +0 -0
  34. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/prompts/__init__.py +0 -0
  35. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/prompts/system_prompt.py +0 -0
  36. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/py.typed +0 -0
  37. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/redis_storage.py +0 -0
  38. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/rossum_mcp_integration.py +0 -0
  39. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/skills/hook-debugging.md +0 -0
  40. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/skills/organization-setup.md +0 -0
  41. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/skills/rossum-deployment.md +0 -0
  42. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/skills/schema-patching.md +0 -0
  43. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/skills/schema-pruning.md +0 -0
  44. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/skills/ui-settings.md +0 -0
  45. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/streamlit_app/__init__.py +0 -0
  46. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/streamlit_app/app.py +0 -0
  47. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/streamlit_app/beep_sound.py +0 -0
  48. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/streamlit_app/cli.py +0 -0
  49. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/streamlit_app/render_modules.py +0 -0
  50. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/streamlit_app/response_formatting.py +0 -0
  51. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/__init__.py +0 -0
  52. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/core.py +0 -0
  53. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/deploy.py +0 -0
  54. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/dynamic_tools.py +0 -0
  55. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/file_tools.py +0 -0
  56. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/formula.py +0 -0
  57. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/skills.py +0 -0
  58. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/spawn_mcp.py +0 -0
  59. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/subagents/__init__.py +0 -0
  60. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/subagents/base.py +0 -0
  61. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/subagents/hook_debug.py +0 -0
  62. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/subagents/knowledge_base.py +0 -0
  63. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/subagents/mcp_helpers.py +0 -0
  64. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/tools/subagents/schema_patching.py +0 -0
  65. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/url_context.py +0 -0
  66. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/user_detection.py +0 -0
  67. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent/utils.py +0 -0
  68. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent.egg-info/dependency_links.txt +0 -0
  69. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent.egg-info/entry_points.txt +0 -0
  70. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent.egg-info/requires.txt +0 -0
  71. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/rossum_agent.egg-info/top_level.txt +0 -0
  72. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/setup.cfg +0 -0
  73. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_agent_logging.py +0 -0
  74. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_app_llm_response_formatting.py +0 -0
  75. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_bedrock_client.py +0 -0
  76. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_beep_sound.py +0 -0
  77. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_dynamic_tools.py +0 -0
  78. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_redis_storage.py +0 -0
  79. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_rossum_mcp_integration.py +0 -0
  80. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_smoke.py +0 -0
  81. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_url_context.py +0 -0
  82. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_user_detection.py +0 -0
  83. {rossum_agent-1.0.0rc3 → rossum_agent-1.0.0rc5}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rossum-agent
3
- Version: 1.0.0rc3
3
+ Version: 1.0.0rc5
4
4
  Summary: AI agent toolkit for Rossum: document workflows conversationally, debug pipelines automatically, and enable agentic configuration of intelligent document processing.
5
5
  Author-email: "Dan Stancl (Rossum AI)" <daniel.stancl@gmail.com>
6
6
  License: MIT
@@ -161,6 +161,7 @@ uv sync --extra streamlit # Streamlit UI only
161
161
  | `AWS_DEFAULT_REGION` | No | AWS region (default: `us-east-1`) |
162
162
  | `REDIS_HOST` | No | Redis host for chat persistence |
163
163
  | `REDIS_PORT` | No | Redis port (default: `6379`) |
164
+ | `ROSSUM_MCP_MODE` | No | MCP mode: `read-only` (default) or `read-write` |
164
165
 
165
166
  ## Usage
166
167
 
@@ -298,6 +299,10 @@ flowchart TB
298
299
 
299
300
  API docs: `/api/docs` (Swagger) or `/api/redoc`
300
301
 
302
+ **MCP Mode:** Chat sessions support mode switching via the `mcp_mode` parameter:
303
+ - Set at chat creation: `POST /api/v1/chats` with `{"mcp_mode": "read-write"}`
304
+ - Override per message: `POST /api/v1/chats/{id}/messages` with `{"content": "...", "mcp_mode": "read-write"}`
305
+
301
306
  </details>
302
307
 
303
308
  ## License
@@ -80,6 +80,7 @@ uv sync --extra streamlit # Streamlit UI only
80
80
  | `AWS_DEFAULT_REGION` | No | AWS region (default: `us-east-1`) |
81
81
  | `REDIS_HOST` | No | Redis host for chat persistence |
82
82
  | `REDIS_PORT` | No | Redis port (default: `6379`) |
83
+ | `ROSSUM_MCP_MODE` | No | MCP mode: `read-only` (default) or `read-write` |
83
84
 
84
85
  ## Usage
85
86
 
@@ -217,6 +218,10 @@ flowchart TB
217
218
 
218
219
  API docs: `/api/docs` (Swagger) or `/api/redoc`
219
220
 
221
+ **MCP Mode:** Chat sessions support mode switching via the `mcp_mode` parameter:
222
+ - Set at chat creation: `POST /api/v1/chats` with `{"mcp_mode": "read-write"}`
223
+ - Override per message: `POST /api/v1/chats/{id}/messages` with `{"content": "...", "mcp_mode": "read-write"}`
224
+
220
225
  </details>
221
226
 
222
227
  ## License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rossum-agent"
7
- version = "1.0.0rc3"
7
+ version = "1.0.0rc5"
8
8
  description = "AI agent toolkit for Rossum: document workflows conversationally, debug pipelines automatically, and enable agentic configuration of intelligent document processing."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -4,6 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  from rossum_agent.agent import AgentConfig, AgentStep, RossumAgent, create_agent
6
6
 
7
- __version__ = "1.0.0rc3"
7
+ __version__ = "1.0.0rc5"
8
8
 
9
9
  __all__ = ["AgentConfig", "AgentStep", "RossumAgent", "create_agent"]
@@ -75,7 +75,6 @@ from rossum_agent.agent.models import (
75
75
  ToolResult,
76
76
  truncate_content,
77
77
  )
78
- from rossum_agent.agent.request_classifier import RequestScope, classify_request, generate_rejection_response
79
78
  from rossum_agent.api.models.schemas import TokenUsageBreakdown
80
79
  from rossum_agent.bedrock_client import create_bedrock_client, get_model_id
81
80
  from rossum_agent.rossum_mcp_integration import MCPConnection, mcp_tools_to_anthropic_format
@@ -769,32 +768,6 @@ class RossumAgent:
769
768
  text_parts.append(text)
770
769
  return " ".join(text_parts)
771
770
 
772
- def _check_request_scope(self, prompt: UserContent) -> AgentStep | None:
773
- """Check if request is in scope, return rejection step if out of scope."""
774
- text = self._extract_text_from_prompt(prompt)
775
- result = classify_request(self.client, text)
776
- self._total_input_tokens += result.input_tokens
777
- self._total_output_tokens += result.output_tokens
778
- self._main_agent_input_tokens += result.input_tokens
779
- self._main_agent_output_tokens += result.output_tokens
780
- if result.scope == RequestScope.OUT_OF_SCOPE:
781
- rejection = generate_rejection_response(self.client, text)
782
- total_input = result.input_tokens + rejection.input_tokens
783
- total_output = result.output_tokens + rejection.output_tokens
784
- self._total_input_tokens += rejection.input_tokens
785
- self._total_output_tokens += rejection.output_tokens
786
- self._main_agent_input_tokens += rejection.input_tokens
787
- self._main_agent_output_tokens += rejection.output_tokens
788
- return AgentStep(
789
- step_number=1,
790
- final_answer=rejection.response,
791
- is_final=True,
792
- input_tokens=total_input,
793
- output_tokens=total_output,
794
- step_type=StepType.FINAL_ANSWER,
795
- )
796
- return None
797
-
798
771
  def _inject_preload_info(self, prompt: UserContent, preload_result: str) -> UserContent:
799
772
  """Inject preload result info into the user prompt."""
800
773
  suffix = (
@@ -820,10 +793,6 @@ class RossumAgent:
820
793
 
821
794
  Rate limiting is handled with exponential backoff and jitter.
822
795
  """
823
- if rejection := self._check_request_scope(prompt):
824
- yield rejection
825
- return
826
-
827
796
  loop = asyncio.get_event_loop()
828
797
  mcp_mode = get_mcp_mode()
829
798
  set_mcp_connection(self.mcp_connection, loop, mcp_mode)
@@ -133,6 +133,10 @@ class MessageRequest(BaseModel):
133
133
  description="Optional list of PDF documents (max 5) to include with the message",
134
134
  )
135
135
  rossum_url: str | None = Field(default=None, description="Optional Rossum app URL for context")
136
+ mcp_mode: Literal["read-only", "read-write"] | None = Field(
137
+ default=None,
138
+ description="MCP mode to use for this message and all subsequent messages. If not specified, uses the chat's current mode.",
139
+ )
136
140
 
137
141
 
138
142
  class StepEvent(BaseModel):
@@ -157,6 +157,11 @@ async def send_message(
157
157
 
158
158
  history = chat_data.messages
159
159
  mcp_mode = chat_data.metadata.mcp_mode
160
+ # Use message-level mode if provided, otherwise use chat's mode
161
+ if message.mcp_mode is not None:
162
+ mcp_mode = message.mcp_mode
163
+ # Update chat metadata with new mode for future messages
164
+ chat_data.metadata.mcp_mode = mcp_mode
160
165
  user_prompt = message.content
161
166
  images: list[ImageContent] | None = message.images
162
167
  documents: list[DocumentContent] | None = message.documents
@@ -3,7 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import contextvars
6
7
  import logging
8
+ from dataclasses import dataclass
7
9
  from typing import TYPE_CHECKING, Any, Literal
8
10
 
9
11
  from rossum_agent.agent.core import RossumAgent, create_agent
@@ -43,6 +45,18 @@ if TYPE_CHECKING:
43
45
  logger = logging.getLogger(__name__)
44
46
 
45
47
 
48
+ @dataclass
49
+ class _RequestContext:
50
+ """Per-request context for agent execution."""
51
+
52
+ output_dir: Path | None = None
53
+ sub_agent_queue: asyncio.Queue[SubAgentProgressEvent | SubAgentTextEvent] | None = None
54
+ last_memory: AgentMemory | None = None
55
+
56
+
57
+ _request_context: contextvars.ContextVar[_RequestContext] = contextvars.ContextVar("request_context")
58
+
59
+
46
60
  def convert_sub_agent_progress_to_event(progress: SubAgentProgress) -> SubAgentProgressEvent:
47
61
  """Convert a SubAgentProgress to a SubAgentProgressEvent for SSE streaming.
48
62
 
@@ -133,28 +147,36 @@ class AgentService:
133
147
  """Service for running the Rossum Agent.
134
148
 
135
149
  Manages MCP connection lifecycle and agent execution for API requests.
150
+ Uses contextvars for per-request state to support concurrent requests.
136
151
  """
137
152
 
138
153
  def __init__(self) -> None:
139
154
  """Initialize agent service."""
140
- self._output_dir: Path | None = None
141
- self._sub_agent_queue: asyncio.Queue[SubAgentProgressEvent | SubAgentTextEvent] | None = None
142
- self._last_memory: AgentMemory | None = None
155
+
156
+ def _get_context(self) -> _RequestContext:
157
+ """Get the current request context, creating if needed."""
158
+ try:
159
+ return _request_context.get()
160
+ except LookupError:
161
+ ctx = _RequestContext()
162
+ _request_context.set(ctx)
163
+ return ctx
143
164
 
144
165
  @property
145
166
  def output_dir(self) -> Path | None:
146
167
  """Get the output directory for the current run."""
147
- return self._output_dir
168
+ return self._get_context().output_dir
148
169
 
149
170
  def _on_sub_agent_progress(self, progress: SubAgentProgress) -> None:
150
171
  """Callback for sub-agent progress updates.
151
172
 
152
173
  Converts the progress to an event and puts it on the queue for streaming.
153
174
  """
154
- if self._sub_agent_queue is not None:
175
+ ctx = self._get_context()
176
+ if ctx.sub_agent_queue is not None:
155
177
  event = convert_sub_agent_progress_to_event(progress)
156
178
  try:
157
- self._sub_agent_queue.put_nowait(event)
179
+ ctx.sub_agent_queue.put_nowait(event)
158
180
  except asyncio.QueueFull:
159
181
  logger.warning("Sub-agent progress queue full, dropping event")
160
182
 
@@ -163,10 +185,11 @@ class AgentService:
163
185
 
164
186
  Converts the text to an event and puts it on the queue for streaming.
165
187
  """
166
- if self._sub_agent_queue is not None:
188
+ ctx = self._get_context()
189
+ if ctx.sub_agent_queue is not None:
167
190
  event = SubAgentTextEvent(tool_name=text.tool_name, text=text.text, is_final=text.is_final)
168
191
  try:
169
- self._sub_agent_queue.put_nowait(event)
192
+ ctx.sub_agent_queue.put_nowait(event)
170
193
  except asyncio.QueueFull:
171
194
  logger.warning("Sub-agent text queue full, dropping event")
172
195
 
@@ -196,16 +219,19 @@ class AgentService:
196
219
  if documents:
197
220
  logger.info(f"Including {len(documents)} documents in the prompt")
198
221
 
199
- self._output_dir = create_session_output_dir()
200
- set_session_output_dir(self._output_dir)
201
- set_output_dir(self._output_dir)
222
+ ctx = _RequestContext()
223
+ _request_context.set(ctx)
224
+
225
+ ctx.output_dir = create_session_output_dir()
226
+ set_session_output_dir(ctx.output_dir)
227
+ set_output_dir(ctx.output_dir)
202
228
  set_rossum_credentials(rossum_api_base_url, rossum_api_token)
203
- logger.info(f"Created session output directory: {self._output_dir}")
229
+ logger.info(f"Created session output directory: {ctx.output_dir}")
204
230
 
205
231
  if documents:
206
232
  self._save_documents_to_output_dir(documents)
207
233
 
208
- self._sub_agent_queue = asyncio.Queue(maxsize=100)
234
+ ctx.sub_agent_queue = asyncio.Queue(maxsize=100)
209
235
  set_progress_callback(self._on_sub_agent_progress)
210
236
  set_text_callback(self._on_sub_agent_text)
211
237
 
@@ -237,9 +263,9 @@ class AgentService:
237
263
 
238
264
  try:
239
265
  async for step in agent.run(user_content):
240
- while not self._sub_agent_queue.empty():
266
+ while not ctx.sub_agent_queue.empty():
241
267
  try:
242
- sub_event = self._sub_agent_queue.get_nowait()
268
+ sub_event = ctx.sub_agent_queue.get_nowait()
243
269
  yield sub_event
244
270
  except asyncio.QueueEmpty:
245
271
  break
@@ -251,14 +277,14 @@ class AgentService:
251
277
  total_input_tokens = agent._total_input_tokens
252
278
  total_output_tokens = agent._total_output_tokens
253
279
 
254
- while not self._sub_agent_queue.empty():
280
+ while not ctx.sub_agent_queue.empty():
255
281
  try:
256
- sub_event = self._sub_agent_queue.get_nowait()
282
+ sub_event = ctx.sub_agent_queue.get_nowait()
257
283
  yield sub_event
258
284
  except asyncio.QueueEmpty:
259
285
  break
260
286
 
261
- self._last_memory = agent.memory
287
+ ctx.last_memory = agent.memory
262
288
 
263
289
  yield StreamDoneEvent(
264
290
  total_steps=total_steps,
@@ -281,7 +307,6 @@ class AgentService:
281
307
  set_text_callback(None)
282
308
  set_output_dir(None)
283
309
  set_rossum_credentials(None, None)
284
- self._sub_agent_queue = None
285
310
 
286
311
  def _save_documents_to_output_dir(self, documents: list[DocumentContent]) -> None:
287
312
  """Save uploaded documents to the output directory.
@@ -291,12 +316,13 @@ class AgentService:
291
316
  """
292
317
  import base64 # noqa: PLC0415 - import here to avoid circular import at module level
293
318
 
294
- if self._output_dir is None:
319
+ ctx = self._get_context()
320
+ if ctx.output_dir is None:
295
321
  logger.warning("Cannot save documents: output directory not set")
296
322
  return
297
323
 
298
324
  for doc in documents:
299
- file_path = self._output_dir / doc.filename
325
+ file_path = ctx.output_dir / doc.filename
300
326
  try:
301
327
  file_data = base64.b64decode(doc.data)
302
328
  file_path.write_bytes(file_data)
@@ -320,6 +346,7 @@ class AgentService:
320
346
  if not images and not documents:
321
347
  return prompt
322
348
 
349
+ ctx = self._get_context()
323
350
  content: list[ImageBlockParam | TextBlockParam] = []
324
351
  if images:
325
352
  for img in images:
@@ -333,8 +360,8 @@ class AgentService:
333
360
  },
334
361
  }
335
362
  )
336
- if documents and self._output_dir:
337
- doc_paths = [str(self._output_dir / doc.filename) for doc in documents]
363
+ if documents and ctx.output_dir:
364
+ doc_paths = [str(ctx.output_dir / doc.filename) for doc in documents]
338
365
  doc_info = "\n".join(f"- {path}" for path in doc_paths)
339
366
  content.append({"type": "text", "text": f"[Uploaded documents available for processing:\n{doc_info}]"})
340
367
  content.append({"type": "text", "text": prompt})
@@ -416,9 +443,10 @@ class AgentService:
416
443
  images: Optional list of images included with the user prompt.
417
444
  documents: Optional list of documents included with the user prompt.
418
445
  """
419
- if self._last_memory is not None:
446
+ ctx = self._get_context()
447
+ if ctx.last_memory is not None:
420
448
  lean_history: list[dict[str, Any]] = []
421
- for step_dict in self._last_memory.to_dict():
449
+ for step_dict in ctx.last_memory.to_dict():
422
450
  if step_dict.get("type") == "task_step":
423
451
  lean_history.append(step_dict)
424
452
  elif step_dict.get("type") == "memory_step":
@@ -5,7 +5,7 @@ Optimized for Opus 4.5: Goals + constraints, not procedures.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- ROSSUM_EXPERT_INTRO = """You are an expert Rossum platform specialist. Help users understand, document, debug, and configure document processing workflows.
8
+ ROSSUM_EXPERT_INTRO = """You are an expert Rossum platform specialist. Help users understand, document, debug, and configure document processing workflows. Politely redirect requests unrelated to Rossum.
9
9
 
10
10
  **CRITICAL - Use `search_knowledge_base` before**:
11
11
  - Explaining ANY extension/hook behavior (except simple function hooks you can read directly)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rossum-agent
3
- Version: 1.0.0rc3
3
+ Version: 1.0.0rc5
4
4
  Summary: AI agent toolkit for Rossum: document workflows conversationally, debug pipelines automatically, and enable agentic configuration of intelligent document processing.
5
5
  Author-email: "Dan Stancl (Rossum AI)" <daniel.stancl@gmail.com>
6
6
  License: MIT
@@ -161,6 +161,7 @@ uv sync --extra streamlit # Streamlit UI only
161
161
  | `AWS_DEFAULT_REGION` | No | AWS region (default: `us-east-1`) |
162
162
  | `REDIS_HOST` | No | Redis host for chat persistence |
163
163
  | `REDIS_PORT` | No | Redis port (default: `6379`) |
164
+ | `ROSSUM_MCP_MODE` | No | MCP mode: `read-only` (default) or `read-write` |
164
165
 
165
166
  ## Usage
166
167
 
@@ -298,6 +299,10 @@ flowchart TB
298
299
 
299
300
  API docs: `/api/docs` (Swagger) or `/api/redoc`
300
301
 
302
+ **MCP Mode:** Chat sessions support mode switching via the `mcp_mode` parameter:
303
+ - Set at chat creation: `POST /api/v1/chats` with `{"mcp_mode": "read-write"}`
304
+ - Override per message: `POST /api/v1/chats/{id}/messages` with `{"content": "...", "mcp_mode": "read-write"}`
305
+
301
306
  </details>
302
307
 
303
308
  ## License
@@ -20,7 +20,6 @@ rossum_agent/agent/__init__.py
20
20
  rossum_agent/agent/core.py
21
21
  rossum_agent/agent/memory.py
22
22
  rossum_agent/agent/models.py
23
- rossum_agent/agent/request_classifier.py
24
23
  rossum_agent/agent/skills.py
25
24
  rossum_agent/agent/types.py
26
25
  rossum_agent/api/__init__.py
@@ -1,152 +0,0 @@
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}")
File without changes