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,51 @@
1
+ """CLI for testing API SSE events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+
9
+ import httpx
10
+
11
+
12
+ def main() -> None:
13
+ """Send a prompt to the API and print SSE events."""
14
+ parser = argparse.ArgumentParser(description="Test Rossum Agent API SSE events")
15
+ parser.add_argument("prompt", nargs="?", help="Prompt to send")
16
+ parser.add_argument("--api-url", default="http://127.0.0.1:8000", help="API base URL")
17
+ args = parser.parse_args()
18
+
19
+ token = os.environ.get("ROSSUM_API_TOKEN")
20
+ rossum_url = os.environ.get("ROSSUM_API_BASE_URL")
21
+
22
+ if not token:
23
+ print("Error: ROSSUM_API_TOKEN is required.", file=sys.stderr)
24
+ sys.exit(1)
25
+ if not rossum_url:
26
+ print("Error: ROSSUM_API_BASE_URL is required.", file=sys.stderr)
27
+ sys.exit(1)
28
+ assert token and rossum_url
29
+
30
+ prompt = args.prompt or input("Prompt: ")
31
+ headers: dict[str, str] = {"X-Rossum-Token": token, "X-Rossum-Api-Url": rossum_url}
32
+
33
+ with httpx.Client(timeout=300) as client:
34
+ resp = client.post(f"{args.api_url}/api/v1/chats", headers=headers)
35
+ resp.raise_for_status()
36
+ data = resp.json()
37
+ chat_id = data.get("id") or data.get("chat_id")
38
+ print(f"Created chat: {chat_id} (response: {data})\n")
39
+
40
+ print(f"{'=' * 60}\nSSE EVENTS:\n{'=' * 60}")
41
+ with client.stream(
42
+ "POST", f"{args.api_url}/api/v1/chats/{chat_id}/messages", headers=headers, json={"content": prompt}
43
+ ) as response:
44
+ response.raise_for_status()
45
+ for line in response.iter_lines():
46
+ if line:
47
+ print(line)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
@@ -0,0 +1,190 @@
1
+ """FastAPI dependencies for the API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import re
9
+ from dataclasses import dataclass
10
+ from typing import Annotated # noqa: TC003 - Required at runtime for FastAPI dependency injection
11
+ from urllib.parse import urlparse
12
+
13
+ import httpx
14
+ from fastapi import Header, HTTPException, status
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Base allowed hosts pattern
19
+ _BASE_ALLOWED_HOSTS = (
20
+ r"elis\.rossum\.ai|api\.elis\.rossum\.ai|(.*\.)?api\.rossum\.ai|.*\.rossum\.app|(elis|api\.elis)\.develop\.r8\.lol"
21
+ )
22
+
23
+ # Additional hosts from environment variable (comma-separated regex patterns)
24
+ # Example: ADDITIONAL_ALLOWED_ROSSUM_HOSTS=".*\.review\.r8\.lol,.*\.staging\.example\.com"
25
+ _ADDITIONAL_HOSTS = os.environ.get("ADDITIONAL_ALLOWED_ROSSUM_HOSTS", "")
26
+
27
+
28
+ def _build_allowed_hosts_pattern() -> re.Pattern[str]:
29
+ """Build the allowed hosts regex pattern including any additional hosts."""
30
+ patterns = [_BASE_ALLOWED_HOSTS]
31
+ if _ADDITIONAL_HOSTS:
32
+ additional = [p.strip() for p in _ADDITIONAL_HOSTS.split(",") if p.strip()]
33
+ patterns.extend(additional)
34
+ return re.compile(f"^({'|'.join(patterns)})$")
35
+
36
+
37
+ ALLOWED_ROSSUM_HOST_PATTERN = _build_allowed_hosts_pattern()
38
+
39
+
40
+ def validate_rossum_api_url(url: str) -> str:
41
+ """Validate that the Rossum API URL is a trusted Rossum domain.
42
+
43
+ This prevents SSRF attacks by ensuring we only make requests to known Rossum endpoints.
44
+
45
+ Args:
46
+ url: The API URL to validate.
47
+
48
+ Returns:
49
+ The validated and normalized API base URL.
50
+
51
+ Raises:
52
+ HTTPException: If the URL is not a valid Rossum API endpoint.
53
+ """
54
+ try:
55
+ parsed = urlparse(url)
56
+ except ValueError as e:
57
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid X-Rossum-Api-Url format") from e
58
+
59
+ if parsed.scheme != "https":
60
+ raise HTTPException(
61
+ status_code=status.HTTP_400_BAD_REQUEST,
62
+ detail="X-Rossum-Api-Url must use HTTPS",
63
+ )
64
+
65
+ if not parsed.hostname:
66
+ raise HTTPException(
67
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid X-Rossum-Api-Url: missing hostname"
68
+ )
69
+
70
+ if not ALLOWED_ROSSUM_HOST_PATTERN.match(parsed.hostname):
71
+ logger.warning(f"Rejected non-Rossum API URL: {parsed.hostname}")
72
+ raise HTTPException(
73
+ status_code=status.HTTP_400_BAD_REQUEST,
74
+ detail="X-Rossum-Api-Url must be a valid Rossum API endpoint",
75
+ )
76
+
77
+ api_base = f"{parsed.scheme}://{parsed.hostname}"
78
+ if parsed.port and parsed.port != 443:
79
+ api_base = f"{api_base}:{parsed.port}"
80
+
81
+ # Preserve /api prefix if present (some Rossum environments use /api/v1 path)
82
+ if parsed.path:
83
+ path = parsed.path.rstrip("/")
84
+ # Strip /v1 suffix to avoid duplication when we append /v1/auth/user
85
+ if path.endswith("/v1"):
86
+ path = path[:-3]
87
+ if path:
88
+ api_base = f"{api_base}{path}"
89
+
90
+ return api_base
91
+
92
+
93
+ @dataclass
94
+ class RossumCredentials:
95
+ """Rossum API credentials extracted from request headers."""
96
+
97
+ token: str
98
+ api_url: str
99
+ user_id: str | None = None
100
+
101
+
102
+ async def get_rossum_credentials(
103
+ x_rossum_token: Annotated[str, Header(alias="X-Rossum-Token")],
104
+ x_rossum_api_url: Annotated[str, Header(alias="X-Rossum-Api-Url")],
105
+ ) -> RossumCredentials:
106
+ """Extract and validate Rossum credentials from request headers.
107
+
108
+ Args:
109
+ x_rossum_token: Rossum API token from X-Rossum-Token header.
110
+ x_rossum_api_url: Rossum API URL from X-Rossum-Api-Url header.
111
+
112
+ Returns:
113
+ RossumCredentials with token and API URL.
114
+
115
+ Raises:
116
+ HTTPException: If credentials are missing or invalid.
117
+ """
118
+ if not x_rossum_token:
119
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing X-Rossum-Token header")
120
+ if not x_rossum_api_url:
121
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing X-Rossum-Api-Url header")
122
+
123
+ return RossumCredentials(token=x_rossum_token, api_url=x_rossum_api_url)
124
+
125
+
126
+ async def get_validated_credentials(
127
+ x_rossum_token: Annotated[str, Header(alias="X-Rossum-Token")],
128
+ x_rossum_api_url: Annotated[str, Header(alias="X-Rossum-Api-Url")],
129
+ ) -> RossumCredentials:
130
+ """Extract credentials and validate against Rossum API.
131
+
132
+ Validates the token by calling the Rossum API /v1/auth/user endpoint.
133
+ Extracts user ID from the response for user isolation.
134
+
135
+ Args:
136
+ x_rossum_token: Rossum API token from X-Rossum-Token header.
137
+ x_rossum_api_url: Rossum API URL from X-Rossum-Api-Url header.
138
+
139
+ Returns:
140
+ RossumCredentials with token, API URL, and user_id.
141
+
142
+ Raises:
143
+ HTTPException: If credentials are missing or invalid.
144
+ """
145
+ credentials = await get_rossum_credentials(x_rossum_token, x_rossum_api_url)
146
+
147
+ # Validate and normalize API URL to prevent SSRF
148
+ api_base = validate_rossum_api_url(credentials.api_url)
149
+ # Strip trailing /v1 to avoid duplication (URL might be .../api or .../api/v1)
150
+ api_base_normalized = api_base.rstrip("/")
151
+ if api_base_normalized.endswith("/v1"):
152
+ api_base_normalized = api_base_normalized[:-3]
153
+
154
+ try:
155
+ async with httpx.AsyncClient() as client:
156
+ response = await client.get(
157
+ f"{api_base_normalized}/v1/auth/user",
158
+ headers={"Authorization": f"Bearer {credentials.token}"},
159
+ timeout=10.0,
160
+ )
161
+
162
+ if response.status_code == 401:
163
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Rossum API token")
164
+
165
+ if response.status_code != 200:
166
+ logger.warning(f"Rossum API returned {response.status_code}")
167
+ raise HTTPException(
168
+ status_code=status.HTTP_502_BAD_GATEWAY, detail="Failed to validate token with Rossum API"
169
+ )
170
+
171
+ try:
172
+ user_data = response.json()
173
+ except json.JSONDecodeError as e:
174
+ logger.error(f"Rossum API returned invalid JSON: {e}. Response text: {response.text!r}")
175
+ raise HTTPException(
176
+ status_code=status.HTTP_502_BAD_GATEWAY, detail="Rossum API returned invalid response"
177
+ ) from e
178
+
179
+ user_id = str(user_data.get("id", ""))
180
+
181
+ if not user_id:
182
+ raise HTTPException(
183
+ status_code=status.HTTP_502_BAD_GATEWAY, detail="Rossum API did not return user ID"
184
+ )
185
+
186
+ return RossumCredentials(token=credentials.token, api_url=credentials.api_url, user_id=user_id)
187
+
188
+ except httpx.RequestError as e:
189
+ logger.error(f"Failed to connect to Rossum API: {e}")
190
+ raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="Failed to connect to Rossum API") from e
@@ -0,0 +1,180 @@
1
+ """FastAPI application entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import os
8
+ import sys
9
+ from contextlib import asynccontextmanager
10
+ from typing import TYPE_CHECKING
11
+
12
+ from fastapi import FastAPI, Request, status
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import AsyncGenerator
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.responses import JSONResponse
18
+ from slowapi import Limiter
19
+ from slowapi.errors import RateLimitExceeded
20
+ from slowapi.util import get_remote_address
21
+ from starlette.middleware.base import BaseHTTPMiddleware
22
+
23
+ from rossum_agent.api.routes import chats, files, health, messages
24
+ from rossum_agent.api.services.agent_service import AgentService
25
+ from rossum_agent.api.services.chat_service import ChatService
26
+ from rossum_agent.api.services.file_service import FileService
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ MAX_REQUEST_SIZE = 10 * 1024 * 1024 # 10 MB (supports image uploads)
31
+
32
+ limiter = Limiter(key_func=get_remote_address)
33
+
34
+
35
+ class RequestSizeLimitMiddleware(BaseHTTPMiddleware):
36
+ """Middleware to limit request body size."""
37
+
38
+ async def dispatch(self, request: Request, call_next):
39
+ content_length = request.headers.get("content-length")
40
+ if content_length and int(content_length) > MAX_REQUEST_SIZE:
41
+ return JSONResponse(
42
+ status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
43
+ content={"detail": f"Request body too large. Maximum size is {MAX_REQUEST_SIZE // 1024} KB."},
44
+ )
45
+ return await call_next(request)
46
+
47
+
48
+ def rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded) -> JSONResponse:
49
+ """Handle rate limit exceeded errors."""
50
+ return JSONResponse(
51
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
52
+ content={"detail": f"Rate limit exceeded: {exc.detail}"},
53
+ )
54
+
55
+
56
+ _chat_service: ChatService | None = None
57
+ _agent_service: AgentService | None = None
58
+ _file_service: FileService | None = None
59
+
60
+
61
+ def get_chat_service() -> ChatService:
62
+ """Get the shared ChatService instance."""
63
+ global _chat_service
64
+ if _chat_service is None:
65
+ _chat_service = ChatService()
66
+ return _chat_service
67
+
68
+
69
+ def get_agent_service() -> AgentService:
70
+ """Get the shared AgentService instance."""
71
+ global _agent_service
72
+ if _agent_service is None:
73
+ _agent_service = AgentService()
74
+ return _agent_service
75
+
76
+
77
+ def get_file_service() -> FileService:
78
+ """Get the shared FileService instance."""
79
+ global _file_service
80
+ if _file_service is None:
81
+ _file_service = FileService(get_chat_service().storage)
82
+ return _file_service
83
+
84
+
85
+ @asynccontextmanager
86
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
87
+ """Lifespan context manager for startup and shutdown events."""
88
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
89
+ logger.info("Rossum Agent API starting up...")
90
+
91
+ chat_service = get_chat_service()
92
+ if chat_service.is_connected():
93
+ logger.info("Redis connection established")
94
+ else:
95
+ logger.warning("Redis connection failed - some features may not work")
96
+
97
+ yield
98
+
99
+ logger.info("Rossum Agent API shutting down...")
100
+ if _chat_service is not None:
101
+ _chat_service.storage.close()
102
+
103
+
104
+ app = FastAPI(
105
+ title="Rossum Agent API",
106
+ description="REST API for Rossum Agent - AI-powered document processing assistant",
107
+ version="0.2.0",
108
+ docs_url="/api/docs",
109
+ redoc_url="/api/redoc",
110
+ openapi_url="/api/openapi.json",
111
+ lifespan=lifespan,
112
+ )
113
+
114
+ app.state.limiter = limiter
115
+ app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler)
116
+
117
+ app.add_middleware(RequestSizeLimitMiddleware)
118
+
119
+
120
+ def _build_cors_origin_regex() -> str:
121
+ """Build CORS origin regex including any additional allowed hosts."""
122
+ patterns = [r".*\.rossum\.app"]
123
+ additional_hosts = os.environ.get("ADDITIONAL_ALLOWED_ROSSUM_HOSTS", "")
124
+ if additional_hosts:
125
+ patterns.extend(p.strip() for p in additional_hosts.split(",") if p.strip())
126
+ return rf"https://({'|'.join(patterns)})"
127
+
128
+
129
+ app.add_middleware(
130
+ CORSMiddleware,
131
+ allow_origins=[
132
+ "https://elis.rossum.ai",
133
+ "https://elis.develop.r8.lol",
134
+ ],
135
+ allow_origin_regex=_build_cors_origin_regex(),
136
+ allow_credentials=True,
137
+ allow_methods=["*"],
138
+ allow_headers=["*"],
139
+ )
140
+
141
+ health.set_chat_service_getter(get_chat_service)
142
+ chats.set_chat_service_getter(get_chat_service)
143
+ messages.set_chat_service_getter(get_chat_service)
144
+ messages.set_agent_service_getter(get_agent_service)
145
+ files.set_chat_service_getter(get_chat_service)
146
+ files.set_file_service_getter(get_file_service)
147
+
148
+ app.include_router(health.router, prefix="/api/v1")
149
+ app.include_router(chats.router, prefix="/api/v1")
150
+ app.include_router(messages.router, prefix="/api/v1")
151
+ app.include_router(files.router, prefix="/api/v1")
152
+
153
+
154
+ def main() -> None:
155
+ """CLI entry point for the API server."""
156
+ parser = argparse.ArgumentParser(description="Run the Rossum Agent API server")
157
+ parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
158
+ parser.add_argument("--port", type=int, default=8000, help="Port to listen on (default: 8000)")
159
+ parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
160
+ parser.add_argument("--workers", type=int, default=1, help="Number of worker processes (default: 1)")
161
+
162
+ args = parser.parse_args()
163
+
164
+ try:
165
+ import uvicorn # noqa: PLC0415
166
+ except ImportError:
167
+ print("Error: uvicorn is required. Install with: uv pip install 'rossum-agent[api]'")
168
+ sys.exit(1)
169
+
170
+ uvicorn.run(
171
+ "rossum_agent.api.main:app",
172
+ host=args.host,
173
+ port=args.port,
174
+ reload=args.reload,
175
+ workers=args.workers if not args.reload else 1,
176
+ )
177
+
178
+
179
+ if __name__ == "__main__":
180
+ main()
@@ -0,0 +1 @@
1
+ """Pydantic models for API requests and responses."""