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,301 @@
1
+ """Pydantic models for API requests and responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+
11
+ class CreateChatRequest(BaseModel):
12
+ """Request body for creating a new chat session."""
13
+
14
+ mcp_mode: Literal["read-only", "read-write"] = "read-only"
15
+
16
+
17
+ class ChatResponse(BaseModel):
18
+ """Response for chat creation."""
19
+
20
+ chat_id: str
21
+ created_at: datetime
22
+
23
+
24
+ class ChatSummary(BaseModel):
25
+ """Summary of a chat session for list responses."""
26
+
27
+ chat_id: str
28
+ timestamp: int
29
+ message_count: int
30
+ first_message: str
31
+ preview: str | None = None
32
+
33
+
34
+ class ChatListResponse(BaseModel):
35
+ """Response for listing chat sessions."""
36
+
37
+ chats: list[ChatSummary]
38
+ total: int
39
+ limit: int
40
+ offset: int
41
+
42
+
43
+ class ImageContent(BaseModel):
44
+ """Image content in a message."""
45
+
46
+ type: Literal["image"] = "image"
47
+ media_type: Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
48
+ data: str = Field(..., description="Base64-encoded image data")
49
+
50
+ @field_validator("data")
51
+ @classmethod
52
+ def validate_base64_size(cls, v: str) -> str:
53
+ max_size = 5 * 1024 * 1024 # 5 MB limit for base64 data
54
+ if len(v) > max_size * 4 // 3: # Base64 is ~4/3 larger than binary
55
+ msg = "Image data exceeds maximum size of 5 MB"
56
+ raise ValueError(msg)
57
+ return v
58
+
59
+
60
+ class DocumentContent(BaseModel):
61
+ """Document content in a message."""
62
+
63
+ type: Literal["document"] = "document"
64
+ media_type: Literal["application/pdf"]
65
+ data: str = Field(..., description="Base64-encoded document data")
66
+ filename: str = Field(..., description="Original filename of the document")
67
+
68
+ @field_validator("data")
69
+ @classmethod
70
+ def validate_base64_size(cls, v: str) -> str:
71
+ max_size = 20 * 1024 * 1024 # 20 MB limit for base64 data
72
+ if len(v) > max_size * 4 // 3:
73
+ msg = "Document data exceeds maximum size of 20 MB"
74
+ raise ValueError(msg)
75
+ return v
76
+
77
+
78
+ class TextContent(BaseModel):
79
+ """Text content in a message."""
80
+
81
+ type: Literal["text"] = "text"
82
+ text: str
83
+
84
+
85
+ class Message(BaseModel):
86
+ """A single chat message."""
87
+
88
+ role: Literal["user", "assistant"]
89
+ content: str | list[TextContent | ImageContent]
90
+
91
+
92
+ class FileInfo(BaseModel):
93
+ """Information about a file in a chat session."""
94
+
95
+ filename: str
96
+ size: int
97
+ timestamp: str
98
+ mime_type: str | None = None
99
+
100
+
101
+ class ChatDetail(BaseModel):
102
+ """Detailed chat session information."""
103
+
104
+ chat_id: str
105
+ messages: list[Message]
106
+ created_at: datetime
107
+ files: list[FileInfo]
108
+
109
+
110
+ class DeleteResponse(BaseModel):
111
+ """Response for delete operations."""
112
+
113
+ deleted: bool
114
+
115
+
116
+ class MessageRequest(BaseModel):
117
+ """Request body for sending a message.
118
+
119
+ Supports text-only messages or multimodal messages with images and documents.
120
+ For image messages, use the `images` field with base64-encoded image data.
121
+ For document messages, use the `documents` field with base64-encoded PDF data.
122
+ """
123
+
124
+ content: str = Field(..., min_length=1, max_length=50000, description="Text content of the message")
125
+ images: list[ImageContent] | None = Field(
126
+ default=None,
127
+ max_length=5,
128
+ description="Optional list of images (max 5) to include with the message",
129
+ )
130
+ documents: list[DocumentContent] | None = Field(
131
+ default=None,
132
+ max_length=5,
133
+ description="Optional list of PDF documents (max 5) to include with the message",
134
+ )
135
+ rossum_url: str | None = Field(default=None, description="Optional Rossum app URL for context")
136
+
137
+
138
+ class StepEvent(BaseModel):
139
+ """Event emitted during agent execution via SSE.
140
+
141
+ Extended thinking mode separates the model's internal reasoning from its final response:
142
+ - "thinking": Model's chain-of-thought reasoning (from thinking blocks)
143
+ - "intermediate": Model's response text before tool calls
144
+ - "final_answer": Final response when no more tool calls needed
145
+ """
146
+
147
+ type: Literal["thinking", "intermediate", "tool_start", "tool_result", "final_answer", "error"]
148
+ step_number: int
149
+ content: str | None = None
150
+ tool_name: str | None = None
151
+ tool_arguments: dict | None = None
152
+ tool_progress: tuple[int, int] | None = None
153
+ result: str | None = None
154
+ is_error: bool = False
155
+ is_streaming: bool = False
156
+ is_final: bool = False
157
+
158
+
159
+ class SubAgentProgressEvent(BaseModel):
160
+ """Event emitted during sub-agent (e.g., debug_hook Opus) execution via SSE."""
161
+
162
+ type: Literal["sub_agent_progress"] = "sub_agent_progress"
163
+ tool_name: str
164
+ iteration: int
165
+ max_iterations: int
166
+ current_tool: str | None = None
167
+ tool_calls: list[str] = Field(default_factory=list)
168
+ status: Literal["thinking", "searching", "analyzing", "running_tool", "completed", "running"] = "running"
169
+
170
+
171
+ class SubAgentTextEvent(BaseModel):
172
+ """Event emitted when sub-agent streams text output via SSE."""
173
+
174
+ type: Literal["sub_agent_text"] = "sub_agent_text"
175
+ tool_name: str
176
+ text: str
177
+ is_final: bool = False
178
+
179
+
180
+ class TokenUsageBySource(BaseModel):
181
+ """Token usage for a specific source (main agent or sub-agent)."""
182
+
183
+ input_tokens: int
184
+ output_tokens: int
185
+ total_tokens: int
186
+
187
+ @classmethod
188
+ def from_counts(cls, input_tokens: int, output_tokens: int) -> TokenUsageBySource:
189
+ """Create from input/output counts, computing total."""
190
+ return cls(input_tokens=input_tokens, output_tokens=output_tokens, total_tokens=input_tokens + output_tokens)
191
+
192
+
193
+ class SubAgentTokenUsageDetail(BaseModel):
194
+ """Token usage breakdown for sub-agents."""
195
+
196
+ input_tokens: int
197
+ output_tokens: int
198
+ total_tokens: int
199
+ by_tool: dict[str, TokenUsageBySource]
200
+
201
+ @classmethod
202
+ def from_counts(
203
+ cls, input_tokens: int, output_tokens: int, by_tool: dict[str, tuple[int, int]]
204
+ ) -> SubAgentTokenUsageDetail:
205
+ """Create from input/output counts, computing total."""
206
+ return cls(
207
+ input_tokens=input_tokens,
208
+ output_tokens=output_tokens,
209
+ total_tokens=input_tokens + output_tokens,
210
+ by_tool={name: TokenUsageBySource.from_counts(inp, out) for name, (inp, out) in by_tool.items()},
211
+ )
212
+
213
+
214
+ class TokenUsageBreakdown(BaseModel):
215
+ """Token usage breakdown by agent vs sub-agents."""
216
+
217
+ total: TokenUsageBySource
218
+ main_agent: TokenUsageBySource
219
+ sub_agents: SubAgentTokenUsageDetail
220
+
221
+ @classmethod
222
+ def from_raw_counts(
223
+ cls,
224
+ total_input: int,
225
+ total_output: int,
226
+ main_input: int,
227
+ main_output: int,
228
+ sub_input: int,
229
+ sub_output: int,
230
+ sub_by_tool: dict[str, tuple[int, int]],
231
+ ) -> TokenUsageBreakdown:
232
+ """Create breakdown from raw token counts."""
233
+ return cls(
234
+ total=TokenUsageBySource.from_counts(total_input, total_output),
235
+ main_agent=TokenUsageBySource.from_counts(main_input, main_output),
236
+ sub_agents=SubAgentTokenUsageDetail.from_counts(sub_input, sub_output, sub_by_tool),
237
+ )
238
+
239
+ def format_summary_lines(self) -> list[str]:
240
+ """Format token usage as human-readable lines."""
241
+ lines = [
242
+ "",
243
+ "=" * 60,
244
+ "TOKEN USAGE SUMMARY",
245
+ "=" * 60,
246
+ f"{'Category':<25} {'Input':>12} {'Output':>12} {'Total':>12}",
247
+ "-" * 60,
248
+ f"{'Main Agent':<25} {self.main_agent.input_tokens:>12,} {self.main_agent.output_tokens:>12,} {self.main_agent.total_tokens:>12,}",
249
+ f"{'Sub-agents (total)':<25} {self.sub_agents.input_tokens:>12,} {self.sub_agents.output_tokens:>12,} {self.sub_agents.total_tokens:>12,}",
250
+ ]
251
+ for tool_name, usage in self.sub_agents.by_tool.items():
252
+ lines.append(
253
+ f" └─ {tool_name:<21} {usage.input_tokens:>12,} {usage.output_tokens:>12,} {usage.total_tokens:>12,}"
254
+ )
255
+ lines.extend(
256
+ [
257
+ "-" * 60,
258
+ f"{'TOTAL':<25} {self.total.input_tokens:>12,} {self.total.output_tokens:>12,} {self.total.total_tokens:>12,}",
259
+ "=" * 60,
260
+ ]
261
+ )
262
+ return lines
263
+
264
+
265
+ class StreamDoneEvent(BaseModel):
266
+ """Final event emitted when streaming completes."""
267
+
268
+ total_steps: int
269
+ input_tokens: int
270
+ output_tokens: int
271
+ token_usage_breakdown: TokenUsageBreakdown | None = None
272
+
273
+
274
+ class FileCreatedEvent(BaseModel):
275
+ """Event emitted when a file is created and stored."""
276
+
277
+ type: Literal["file_created"] = "file_created"
278
+ filename: str
279
+ url: str
280
+
281
+
282
+ class HealthResponse(BaseModel):
283
+ """Response for health check endpoint."""
284
+
285
+ status: Literal["healthy", "unhealthy"]
286
+ redis_connected: bool
287
+ version: str
288
+
289
+
290
+ class ErrorResponse(BaseModel):
291
+ """Standard error response."""
292
+
293
+ detail: str
294
+ error_code: str | None = None
295
+
296
+
297
+ class FileListResponse(BaseModel):
298
+ """Response for listing files in a chat session."""
299
+
300
+ files: list[FileInfo]
301
+ total: int
@@ -0,0 +1 @@
1
+ """API route handlers."""
@@ -0,0 +1,95 @@
1
+ """Chat session CRUD endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable # noqa: TC003 - Required at runtime for service getter type hints
6
+ from typing import Annotated # noqa: TC003 - Required at runtime for FastAPI dependency injection
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
9
+ from slowapi import Limiter
10
+ from slowapi.util import get_remote_address
11
+
12
+ from rossum_agent.api.dependencies import RossumCredentials, get_validated_credentials
13
+ from rossum_agent.api.models.schemas import (
14
+ ChatDetail,
15
+ ChatListResponse,
16
+ ChatResponse,
17
+ CreateChatRequest,
18
+ DeleteResponse,
19
+ )
20
+ from rossum_agent.api.services.chat_service import (
21
+ ChatService, # noqa: TC001 - Required at runtime for FastAPI Depends()
22
+ )
23
+
24
+ limiter = Limiter(key_func=get_remote_address)
25
+
26
+ router = APIRouter(prefix="/chats", tags=["chats"])
27
+
28
+ _get_chat_service: Callable[[], ChatService] | None = None
29
+
30
+
31
+ def set_chat_service_getter(getter: Callable[[], ChatService]) -> None:
32
+ """Set the chat service getter function."""
33
+ global _get_chat_service
34
+ _get_chat_service = getter
35
+
36
+
37
+ def get_chat_service_dep() -> ChatService:
38
+ """Dependency function for chat service."""
39
+ if _get_chat_service is None:
40
+ raise RuntimeError("Chat service getter not configured")
41
+ return _get_chat_service()
42
+
43
+
44
+ @router.post("", response_model=ChatResponse, status_code=status.HTTP_201_CREATED)
45
+ @limiter.limit("30/minute")
46
+ async def create_chat(
47
+ request: Request,
48
+ body: CreateChatRequest | None = None,
49
+ credentials: Annotated[RossumCredentials, Depends(get_validated_credentials)] = None, # type: ignore[assignment]
50
+ chat_service: Annotated[ChatService, Depends(get_chat_service_dep)] = None, # type: ignore[assignment]
51
+ ) -> ChatResponse:
52
+ """Create a new chat session."""
53
+ mcp_mode = body.mcp_mode if body else "read-only"
54
+ return chat_service.create_chat(user_id=credentials.user_id, mcp_mode=mcp_mode)
55
+
56
+
57
+ @router.get("", response_model=ChatListResponse)
58
+ async def list_chats(
59
+ limit: Annotated[int, Query(ge=1, le=100)] = 50,
60
+ offset: Annotated[int, Query(ge=0)] = 0,
61
+ credentials: Annotated[RossumCredentials, Depends(get_validated_credentials)] = None, # type: ignore[assignment]
62
+ chat_service: Annotated[ChatService, Depends(get_chat_service_dep)] = None, # type: ignore[assignment]
63
+ ) -> ChatListResponse:
64
+ """List chat sessions for the authenticated user."""
65
+ return chat_service.list_chats(user_id=credentials.user_id, limit=limit, offset=offset)
66
+
67
+
68
+ @router.get("/{chat_id}", response_model=ChatDetail)
69
+ async def get_chat(
70
+ chat_id: str,
71
+ credentials: Annotated[RossumCredentials, Depends(get_validated_credentials)] = None, # type: ignore[assignment]
72
+ chat_service: Annotated[ChatService, Depends(get_chat_service_dep)] = None, # type: ignore[assignment]
73
+ ) -> ChatDetail:
74
+ """Get detailed information about a chat session."""
75
+ chat = chat_service.get_chat(user_id=credentials.user_id, chat_id=chat_id)
76
+
77
+ if chat is None:
78
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Chat {chat_id} not found")
79
+
80
+ return chat
81
+
82
+
83
+ @router.delete("/{chat_id}", response_model=DeleteResponse)
84
+ async def delete_chat(
85
+ chat_id: str,
86
+ credentials: Annotated[RossumCredentials, Depends(get_validated_credentials)] = None, # type: ignore[assignment]
87
+ chat_service: Annotated[ChatService, Depends(get_chat_service_dep)] = None, # type: ignore[assignment]
88
+ ) -> DeleteResponse:
89
+ """Delete a chat session."""
90
+ if not chat_service.chat_exists(credentials.user_id, chat_id):
91
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Chat {chat_id} not found")
92
+
93
+ deleted = chat_service.delete_chat(user_id=credentials.user_id, chat_id=chat_id)
94
+
95
+ return DeleteResponse(deleted=deleted)
@@ -0,0 +1,113 @@
1
+ """File management endpoints for chat sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable # noqa: TC003 - Required at runtime for service getter type hints
7
+ from pathlib import Path
8
+ from typing import Annotated # noqa: TC003 - Required at runtime for FastAPI dependency injection
9
+
10
+ from fastapi import APIRouter, Depends, HTTPException, status
11
+ from fastapi.responses import Response
12
+
13
+ from rossum_agent.api.dependencies import RossumCredentials, get_validated_credentials
14
+ from rossum_agent.api.models.schemas import FileListResponse
15
+ from rossum_agent.api.services.chat_service import (
16
+ ChatService, # noqa: TC001 - Required at runtime for FastAPI Depends()
17
+ )
18
+ from rossum_agent.api.services.file_service import (
19
+ FileService, # noqa: TC001 - Required at runtime for FastAPI Depends()
20
+ )
21
+
22
+ router = APIRouter(prefix="/chats/{chat_id}/files", tags=["files"])
23
+
24
+ _get_chat_service: Callable[[], ChatService] | None = None
25
+ _get_file_service: Callable[[], FileService] | None = None
26
+
27
+
28
+ def set_chat_service_getter(getter: Callable[[], ChatService]) -> None:
29
+ """Set the chat service getter function."""
30
+ global _get_chat_service
31
+ _get_chat_service = getter
32
+
33
+
34
+ def set_file_service_getter(getter: Callable[[], FileService]) -> None:
35
+ """Set the file service getter function."""
36
+ global _get_file_service
37
+ _get_file_service = getter
38
+
39
+
40
+ def get_chat_service_dep() -> ChatService:
41
+ """Dependency function for chat service."""
42
+ if _get_chat_service is None:
43
+ raise RuntimeError("Chat service getter not configured")
44
+ return _get_chat_service()
45
+
46
+
47
+ def get_file_service_dep() -> FileService:
48
+ """Dependency function for file service."""
49
+ if _get_file_service is None:
50
+ raise RuntimeError("File service getter not configured")
51
+ return _get_file_service()
52
+
53
+
54
+ @router.get("", response_model=FileListResponse)
55
+ async def list_files(
56
+ chat_id: str,
57
+ credentials: Annotated[RossumCredentials, Depends(get_validated_credentials)] = None, # type: ignore[assignment]
58
+ chat_service: Annotated[ChatService, Depends(get_chat_service_dep)] = None, # type: ignore[assignment]
59
+ file_service: Annotated[FileService, Depends(get_file_service_dep)] = None, # type: ignore[assignment]
60
+ ) -> FileListResponse:
61
+ """List all files for a chat session."""
62
+ if not chat_service.chat_exists(credentials.user_id, chat_id):
63
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Chat {chat_id} not found")
64
+
65
+ files = file_service.list_files(chat_id)
66
+ return FileListResponse(files=files, total=len(files))
67
+
68
+
69
+ def _sanitize_filename(filename: str) -> str:
70
+ """Sanitize filename to prevent path traversal and header injection.
71
+
72
+ - Normalizes Windows backslash separators to forward slashes
73
+ - Extracts only the base filename (no directory traversal)
74
+ - Removes control characters, newlines, quotes, and backslashes (header injection prevention)
75
+ - Rejects directory-only names like ".." or "."
76
+ - Limits length to prevent DoS
77
+ """
78
+ normalized = filename.replace("\\", "/")
79
+ safe_name = Path(normalized).name
80
+ safe_name = re.sub(r'[\x00-\x1f\x7f"\\]', "", safe_name)
81
+ if safe_name in ("", ".", ".."):
82
+ return ""
83
+ return safe_name[:255]
84
+
85
+
86
+ @router.get("/{filename:path}")
87
+ async def download_file(
88
+ chat_id: str,
89
+ filename: str,
90
+ credentials: Annotated[RossumCredentials, Depends(get_validated_credentials)] = None, # type: ignore[assignment]
91
+ chat_service: Annotated[ChatService, Depends(get_chat_service_dep)] = None, # type: ignore[assignment]
92
+ file_service: Annotated[FileService, Depends(get_file_service_dep)] = None, # type: ignore[assignment]
93
+ ) -> Response:
94
+ """Download a file from a chat session."""
95
+ if not chat_service.chat_exists(credentials.user_id, chat_id):
96
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Chat {chat_id} not found")
97
+
98
+ safe_filename = _sanitize_filename(filename)
99
+ if not safe_filename:
100
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid filename")
101
+
102
+ result = file_service.get_file(chat_id, safe_filename)
103
+ if result is None:
104
+ raise HTTPException(
105
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"File {safe_filename} not found in chat {chat_id}"
106
+ )
107
+
108
+ content, mime_type = result
109
+ return Response(
110
+ content=content,
111
+ media_type=mime_type,
112
+ headers={"Content-Disposition": f'attachment; filename="{safe_filename}"'},
113
+ )
@@ -0,0 +1,44 @@
1
+ """Health check endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable # noqa: TC003 - Required at runtime for service getter type hints
6
+ from typing import Annotated # noqa: TC003 - Required at runtime for FastAPI dependency injection
7
+
8
+ from fastapi import APIRouter, Depends
9
+
10
+ from rossum_agent.api.models.schemas import HealthResponse
11
+ from rossum_agent.api.services.chat_service import (
12
+ ChatService, # noqa: TC001 - Required at runtime for FastAPI Depends()
13
+ )
14
+
15
+ router = APIRouter(tags=["health"])
16
+
17
+ VERSION = "0.2.0"
18
+
19
+ _get_chat_service: Callable[[], ChatService] | None = None
20
+
21
+
22
+ def set_chat_service_getter(getter: Callable[[], ChatService]) -> None:
23
+ """Set the chat service getter function."""
24
+ global _get_chat_service
25
+ _get_chat_service = getter
26
+
27
+
28
+ def get_chat_service_dep() -> ChatService:
29
+ """Dependency function for chat service."""
30
+ if _get_chat_service is None:
31
+ raise RuntimeError("Chat service getter not configured")
32
+ return _get_chat_service()
33
+
34
+
35
+ @router.get("/health", response_model=HealthResponse)
36
+ async def health_check(
37
+ chat_service: Annotated[ChatService, Depends(get_chat_service_dep)],
38
+ ) -> HealthResponse:
39
+ """Check API health and dependencies."""
40
+ redis_connected = chat_service.is_connected()
41
+
42
+ return HealthResponse(
43
+ status="healthy" if redis_connected else "unhealthy", redis_connected=redis_connected, version=VERSION
44
+ )