remdb 0.3.242__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.
Potentially problematic release.
This version of remdb might be problematic. Click here for more details.
- rem/__init__.py +129 -0
- rem/agentic/README.md +760 -0
- rem/agentic/__init__.py +54 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +38 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +425 -0
- rem/agentic/context_builder.py +360 -0
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +273 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +240 -0
- rem/agentic/providers/phoenix.py +926 -0
- rem/agentic/providers/pydantic_ai.py +854 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +737 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +242 -0
- rem/api/README.md +657 -0
- rem/api/deps.py +253 -0
- rem/api/main.py +460 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +820 -0
- rem/api/mcp_router/server.py +243 -0
- rem/api/mcp_router/tools.py +1605 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +520 -0
- rem/api/routers/auth.py +898 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +702 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +202 -0
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +546 -0
- rem/api/routers/chat/streaming.py +950 -0
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +87 -0
- rem/api/routers/feedback.py +276 -0
- rem/api/routers/messages.py +620 -0
- rem/api/routers/models.py +86 -0
- rem/api/routers/query.py +362 -0
- rem/api/routers/shared_sessions.py +422 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +36 -0
- rem/auth/jwt.py +367 -0
- rem/auth/middleware.py +318 -0
- rem/auth/providers/__init__.py +16 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/email.py +215 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +517 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +299 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +549 -0
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +495 -0
- rem/cli/commands/db.py +828 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1698 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +388 -0
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +230 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/commands/session.py +453 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +123 -0
- rem/config.py +244 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +70 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +672 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +246 -0
- rem/models/entities/__init__.py +68 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +64 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +181 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/session.py +84 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +93 -0
- rem/py.typed +0 -0
- rem/registry.py +373 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +132 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +18 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +760 -0
- rem/services/content/service.py +762 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +322 -0
- rem/services/dreaming/moment_service.py +251 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +127 -0
- rem/services/embeddings/worker.py +435 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +960 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +757 -0
- rem/services/postgres/__init__.py +49 -0
- rem/services/postgres/diff_service.py +599 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
- rem/services/postgres/register_type.py +353 -0
- rem/services/postgres/repository.py +481 -0
- rem/services/postgres/schema_generator.py +661 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +355 -0
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +318 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +180 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +608 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +13 -0
- rem/services/session/compression.py +488 -0
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +85 -0
- rem/services/user_service.py +130 -0
- rem/settings.py +1877 -0
- rem/sql/background_indexes.sql +52 -0
- rem/sql/migrations/001_install.sql +983 -0
- rem/sql/migrations/002_install_models.sql +3157 -0
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +282 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +628 -0
- rem/utils/__init__.py +61 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +436 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/files.py +323 -0
- rem/utils/markdown.py +16 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +492 -0
- rem/utils/schema_loader.py +649 -0
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +350 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +325 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +7 -0
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- rem/workers/unlogged_maintainer.py +463 -0
- remdb-0.3.242.dist-info/METADATA +1632 -0
- remdb-0.3.242.dist-info/RECORD +235 -0
- remdb-0.3.242.dist-info/WHEEL +4 -0
- remdb-0.3.242.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Messages and Sessions endpoints.
|
|
3
|
+
|
|
4
|
+
Provides endpoints for:
|
|
5
|
+
- Listing and filtering messages by date, user_id, session_id
|
|
6
|
+
- Creating and managing sessions (normal or evaluation mode)
|
|
7
|
+
|
|
8
|
+
Endpoints:
|
|
9
|
+
GET /api/v1/messages - List messages with filters
|
|
10
|
+
GET /api/v1/messages/{id} - Get a specific message
|
|
11
|
+
|
|
12
|
+
GET /api/v1/sessions - List sessions
|
|
13
|
+
POST /api/v1/sessions - Create a session
|
|
14
|
+
GET /api/v1/sessions/{id} - Get a specific session
|
|
15
|
+
PUT /api/v1/sessions/{id} - Update a session
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Literal
|
|
21
|
+
from uuid import UUID
|
|
22
|
+
|
|
23
|
+
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
|
|
24
|
+
from loguru import logger
|
|
25
|
+
from pydantic import BaseModel, Field
|
|
26
|
+
|
|
27
|
+
from .common import ErrorResponse
|
|
28
|
+
|
|
29
|
+
from ..deps import (
|
|
30
|
+
get_current_user,
|
|
31
|
+
get_user_filter,
|
|
32
|
+
is_admin,
|
|
33
|
+
require_admin,
|
|
34
|
+
require_auth,
|
|
35
|
+
)
|
|
36
|
+
from ...models.entities import Message, Session, SessionMode
|
|
37
|
+
from ...services.postgres import Repository, get_postgres_service
|
|
38
|
+
from ...settings import settings
|
|
39
|
+
from ...utils.date_utils import parse_iso, utc_now
|
|
40
|
+
|
|
41
|
+
router = APIRouter(prefix="/api/v1")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Enums
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SortOrder(str, Enum):
|
|
50
|
+
"""Sort order for list queries."""
|
|
51
|
+
|
|
52
|
+
ASC = "asc"
|
|
53
|
+
DESC = "desc"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# =============================================================================
|
|
57
|
+
# Request/Response Models
|
|
58
|
+
# =============================================================================
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MessageListResponse(BaseModel):
|
|
62
|
+
"""Response for message list endpoint."""
|
|
63
|
+
|
|
64
|
+
object: Literal["list"] = "list"
|
|
65
|
+
data: list[Message]
|
|
66
|
+
total: int
|
|
67
|
+
has_more: bool
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SessionCreateRequest(BaseModel):
|
|
71
|
+
"""Request to create a new session."""
|
|
72
|
+
|
|
73
|
+
name: str = Field(description="Session name/identifier")
|
|
74
|
+
mode: SessionMode = Field(
|
|
75
|
+
default=SessionMode.NORMAL, description="Session mode: 'normal' or 'evaluation'"
|
|
76
|
+
)
|
|
77
|
+
description: str | None = Field(default=None, description="Session description")
|
|
78
|
+
original_trace_id: str | None = Field(
|
|
79
|
+
default=None,
|
|
80
|
+
description="For evaluation: ID of the original session being evaluated",
|
|
81
|
+
)
|
|
82
|
+
settings_overrides: dict | None = Field(
|
|
83
|
+
default=None,
|
|
84
|
+
description="Settings overrides (model, temperature, max_tokens, system_prompt)",
|
|
85
|
+
)
|
|
86
|
+
prompt: str | None = Field(default=None, description="Custom prompt for this session")
|
|
87
|
+
agent_schema_uri: str | None = Field(
|
|
88
|
+
default=None, description="Agent schema URI for this session"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SessionUpdateRequest(BaseModel):
|
|
93
|
+
"""Request to update a session."""
|
|
94
|
+
|
|
95
|
+
description: str | None = None
|
|
96
|
+
settings_overrides: dict | None = None
|
|
97
|
+
prompt: str | None = None
|
|
98
|
+
message_count: int | None = None
|
|
99
|
+
total_tokens: int | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SessionListResponse(BaseModel):
|
|
103
|
+
"""Response for session list endpoint (deprecated, use SessionsQueryResponse)."""
|
|
104
|
+
|
|
105
|
+
object: Literal["list"] = "list"
|
|
106
|
+
data: list[Session]
|
|
107
|
+
total: int
|
|
108
|
+
has_more: bool
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class SessionWithUser(BaseModel):
|
|
112
|
+
"""Session with user info for admin views."""
|
|
113
|
+
|
|
114
|
+
id: str
|
|
115
|
+
name: str
|
|
116
|
+
mode: str | None = None
|
|
117
|
+
description: str | None = None
|
|
118
|
+
user_id: str | None = None
|
|
119
|
+
user_name: str | None = None
|
|
120
|
+
user_email: str | None = None
|
|
121
|
+
message_count: int = 0
|
|
122
|
+
total_tokens: int | None = None
|
|
123
|
+
created_at: datetime | None = None
|
|
124
|
+
updated_at: datetime | None = None
|
|
125
|
+
metadata: dict | None = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class PaginationMetadata(BaseModel):
|
|
129
|
+
"""Pagination metadata for paginated responses."""
|
|
130
|
+
|
|
131
|
+
total: int = Field(description="Total number of records matching filters")
|
|
132
|
+
page: int = Field(description="Current page number (1-indexed)")
|
|
133
|
+
page_size: int = Field(description="Number of records per page")
|
|
134
|
+
total_pages: int = Field(description="Total number of pages")
|
|
135
|
+
has_next: bool = Field(description="Whether there are more pages after this one")
|
|
136
|
+
has_previous: bool = Field(description="Whether there are pages before this one")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class SessionsQueryResponse(BaseModel):
|
|
140
|
+
"""Response for paginated sessions query."""
|
|
141
|
+
|
|
142
|
+
object: Literal["list"] = "list"
|
|
143
|
+
data: list[SessionWithUser] = Field(description="List of sessions for the current page")
|
|
144
|
+
metadata: PaginationMetadata = Field(description="Pagination metadata")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# =============================================================================
|
|
148
|
+
# Messages Endpoints
|
|
149
|
+
# =============================================================================
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.get(
|
|
153
|
+
"/messages",
|
|
154
|
+
response_model=MessageListResponse,
|
|
155
|
+
tags=["messages"],
|
|
156
|
+
responses={
|
|
157
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
async def list_messages(
|
|
161
|
+
request: Request,
|
|
162
|
+
mine: bool = Query(default=False, description="Only show my messages (uses JWT identity)"),
|
|
163
|
+
user_id: str | None = Query(default=None, description="Filter by user ID (admin only for cross-user)"),
|
|
164
|
+
session_id: str | None = Query(default=None, description="Filter by session ID"),
|
|
165
|
+
start_date: str | None = Query(
|
|
166
|
+
default=None, description="Filter messages after this ISO date"
|
|
167
|
+
),
|
|
168
|
+
end_date: str | None = Query(
|
|
169
|
+
default=None, description="Filter messages before this ISO date"
|
|
170
|
+
),
|
|
171
|
+
message_type: str | None = Query(
|
|
172
|
+
default=None, description="Filter by message type (user, assistant, system, tool)"
|
|
173
|
+
),
|
|
174
|
+
limit: int = Query(default=50, ge=1, le=100, description="Max results to return"),
|
|
175
|
+
offset: int = Query(default=0, ge=0, description="Offset for pagination"),
|
|
176
|
+
sort: SortOrder = Query(default=SortOrder.DESC, description="Sort order by created_at (asc or desc)"),
|
|
177
|
+
) -> MessageListResponse:
|
|
178
|
+
"""
|
|
179
|
+
List messages with optional filters.
|
|
180
|
+
|
|
181
|
+
Access Control:
|
|
182
|
+
- Regular users: Only see their own messages
|
|
183
|
+
- Admin users: Can filter by any user_id or see all messages
|
|
184
|
+
- mine=true: Forces filter to current user (useful for admins to see only their own)
|
|
185
|
+
|
|
186
|
+
Filters can be combined:
|
|
187
|
+
- mine: Only show messages owned by current JWT user (overrides user_id)
|
|
188
|
+
- user_id: Filter by the user who created/owns the message (admin only for cross-user)
|
|
189
|
+
- session_id: Filter by conversation session
|
|
190
|
+
- start_date/end_date: Filter by creation time range (ISO 8601 format)
|
|
191
|
+
- message_type: Filter by role (user, assistant, system, tool)
|
|
192
|
+
- sort: Sort order by created_at (asc or desc, default: desc)
|
|
193
|
+
|
|
194
|
+
Returns paginated results ordered by created_at.
|
|
195
|
+
"""
|
|
196
|
+
if not settings.postgres.enabled:
|
|
197
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
198
|
+
|
|
199
|
+
repo = Repository(Message, table_name="messages")
|
|
200
|
+
|
|
201
|
+
# Get current user for logging
|
|
202
|
+
current_user = get_current_user(request)
|
|
203
|
+
jwt_user_id = current_user.get("id") if current_user else None
|
|
204
|
+
|
|
205
|
+
# If mine=true, force filter to current user's ID from JWT
|
|
206
|
+
effective_user_id = user_id
|
|
207
|
+
if mine:
|
|
208
|
+
if current_user:
|
|
209
|
+
effective_user_id = current_user.get("id")
|
|
210
|
+
|
|
211
|
+
# Build user-scoped filters (admin can see all, regular users see only their own)
|
|
212
|
+
filters = await get_user_filter(request, x_user_id=effective_user_id)
|
|
213
|
+
|
|
214
|
+
# Apply optional filters
|
|
215
|
+
if session_id:
|
|
216
|
+
# session_id is the session UUID - use directly
|
|
217
|
+
filters["session_id"] = session_id
|
|
218
|
+
if message_type:
|
|
219
|
+
filters["message_type"] = message_type
|
|
220
|
+
|
|
221
|
+
# Log the query parameters for debugging
|
|
222
|
+
logger.debug(
|
|
223
|
+
f"[messages] Query: session_id={session_id} | "
|
|
224
|
+
f"jwt_user_id={jwt_user_id} | "
|
|
225
|
+
f"filters={filters}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Build order_by clause based on sort parameter
|
|
229
|
+
order_by = f"created_at {sort.value.upper()}"
|
|
230
|
+
|
|
231
|
+
# For date filtering, we need custom SQL (not supported by basic Repository)
|
|
232
|
+
# For now, fetch all matching base filters and filter in Python
|
|
233
|
+
# TODO: Extend Repository to support date range filters
|
|
234
|
+
messages = await repo.find(
|
|
235
|
+
filters,
|
|
236
|
+
order_by=order_by,
|
|
237
|
+
limit=limit + 1, # Fetch one extra to determine has_more
|
|
238
|
+
offset=offset,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Apply date filters in Python if provided
|
|
242
|
+
if start_date or end_date:
|
|
243
|
+
start_dt = parse_iso(start_date) if start_date else None
|
|
244
|
+
end_dt = parse_iso(end_date) if end_date else None
|
|
245
|
+
|
|
246
|
+
filtered = []
|
|
247
|
+
for msg in messages:
|
|
248
|
+
if start_dt and msg.created_at < start_dt:
|
|
249
|
+
continue
|
|
250
|
+
if end_dt and msg.created_at > end_dt:
|
|
251
|
+
continue
|
|
252
|
+
filtered.append(msg)
|
|
253
|
+
messages = filtered
|
|
254
|
+
|
|
255
|
+
# Determine if there are more results
|
|
256
|
+
has_more = len(messages) > limit
|
|
257
|
+
if has_more:
|
|
258
|
+
messages = messages[:limit]
|
|
259
|
+
|
|
260
|
+
# Get total count for pagination info
|
|
261
|
+
total = await repo.count(filters)
|
|
262
|
+
|
|
263
|
+
# Log result count
|
|
264
|
+
logger.debug(
|
|
265
|
+
f"[messages] Result: returned={len(messages)} | total={total} | "
|
|
266
|
+
f"session_id={session_id}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return MessageListResponse(data=messages, total=total, has_more=has_more)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@router.get(
|
|
273
|
+
"/messages/{message_id}",
|
|
274
|
+
response_model=Message,
|
|
275
|
+
tags=["messages"],
|
|
276
|
+
responses={
|
|
277
|
+
403: {"model": ErrorResponse, "description": "Access denied: not owner"},
|
|
278
|
+
404: {"model": ErrorResponse, "description": "Message not found"},
|
|
279
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
async def get_message(
|
|
283
|
+
request: Request,
|
|
284
|
+
message_id: str,
|
|
285
|
+
) -> Message:
|
|
286
|
+
"""
|
|
287
|
+
Get a specific message by ID.
|
|
288
|
+
|
|
289
|
+
Access Control:
|
|
290
|
+
- Regular users: Only access their own messages
|
|
291
|
+
- Admin users: Can access any message
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
message_id: UUID of the message
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Message object if found
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
404: Message not found
|
|
301
|
+
403: Access denied (not owner and not admin)
|
|
302
|
+
"""
|
|
303
|
+
if not settings.postgres.enabled:
|
|
304
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
305
|
+
|
|
306
|
+
repo = Repository(Message, table_name="messages")
|
|
307
|
+
message = await repo.get_by_id(message_id)
|
|
308
|
+
|
|
309
|
+
if not message:
|
|
310
|
+
raise HTTPException(status_code=404, detail=f"Message '{message_id}' not found")
|
|
311
|
+
|
|
312
|
+
# Check access: admin or owner
|
|
313
|
+
current_user = get_current_user(request)
|
|
314
|
+
if not is_admin(current_user):
|
|
315
|
+
user_id = current_user.get("id") if current_user else None
|
|
316
|
+
if message.user_id and message.user_id != user_id:
|
|
317
|
+
raise HTTPException(status_code=403, detail="Access denied: not owner")
|
|
318
|
+
|
|
319
|
+
return message
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# =============================================================================
|
|
323
|
+
# Sessions Endpoints
|
|
324
|
+
# =============================================================================
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@router.get(
|
|
328
|
+
"/sessions",
|
|
329
|
+
response_model=SessionsQueryResponse,
|
|
330
|
+
tags=["sessions"],
|
|
331
|
+
responses={
|
|
332
|
+
503: {"model": ErrorResponse, "description": "Database not enabled or connection failed"},
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
async def list_sessions(
|
|
336
|
+
request: Request,
|
|
337
|
+
user_id: str | None = Query(default=None, description="Filter by user ID (admin only for cross-user)"),
|
|
338
|
+
user_name: str | None = Query(default=None, description="Filter by user name (partial match, admin only)"),
|
|
339
|
+
user_email: str | None = Query(default=None, description="Filter by user email (partial match, admin only)"),
|
|
340
|
+
mode: SessionMode | None = Query(default=None, description="Filter by session mode"),
|
|
341
|
+
page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
|
|
342
|
+
page_size: int = Query(default=50, ge=1, le=100, description="Number of results per page"),
|
|
343
|
+
) -> SessionsQueryResponse:
|
|
344
|
+
"""
|
|
345
|
+
List sessions with optional filters and page-based pagination.
|
|
346
|
+
|
|
347
|
+
Access Control:
|
|
348
|
+
- Regular users: Only see their own sessions
|
|
349
|
+
- Admin users: Can filter by any user_id, user_name, user_email, or see all sessions
|
|
350
|
+
|
|
351
|
+
Filters:
|
|
352
|
+
- user_id: Filter by session owner (admin only for cross-user)
|
|
353
|
+
- user_name: Filter by user name partial match (admin only)
|
|
354
|
+
- user_email: Filter by user email partial match (admin only)
|
|
355
|
+
- mode: Filter by session mode (normal or evaluation)
|
|
356
|
+
|
|
357
|
+
Pagination:
|
|
358
|
+
- page: Page number (1-indexed, default: 1)
|
|
359
|
+
- page_size: Number of sessions per page (default: 50, max: 100)
|
|
360
|
+
|
|
361
|
+
Returns paginated results with user info ordered by created_at descending.
|
|
362
|
+
"""
|
|
363
|
+
if not settings.postgres.enabled:
|
|
364
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
365
|
+
|
|
366
|
+
current_user = get_current_user(request)
|
|
367
|
+
admin = is_admin(current_user)
|
|
368
|
+
|
|
369
|
+
# Get postgres service for raw SQL query
|
|
370
|
+
db = get_postgres_service()
|
|
371
|
+
if not db:
|
|
372
|
+
raise HTTPException(status_code=503, detail="Database connection failed")
|
|
373
|
+
if not db.pool:
|
|
374
|
+
await db.connect()
|
|
375
|
+
|
|
376
|
+
# Build effective filters based on user role
|
|
377
|
+
effective_user_id = user_id
|
|
378
|
+
effective_user_name = user_name if admin else None # Only admin can search by name
|
|
379
|
+
effective_user_email = user_email if admin else None # Only admin can search by email
|
|
380
|
+
|
|
381
|
+
if not admin:
|
|
382
|
+
# Non-admin users can only see their own sessions
|
|
383
|
+
effective_user_id = current_user.get("id") if current_user else None
|
|
384
|
+
if not effective_user_id:
|
|
385
|
+
# Anonymous user - return empty
|
|
386
|
+
return SessionsQueryResponse(
|
|
387
|
+
data=[],
|
|
388
|
+
metadata=PaginationMetadata(
|
|
389
|
+
total=0, page=page, page_size=page_size,
|
|
390
|
+
total_pages=0, has_next=False, has_previous=False,
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Call the SQL function for sessions with user info
|
|
395
|
+
async with db.pool.acquire() as conn:
|
|
396
|
+
rows = await conn.fetch(
|
|
397
|
+
"""
|
|
398
|
+
SELECT * FROM fn_list_sessions_with_user(
|
|
399
|
+
$1, $2, $3, $4, $5, $6
|
|
400
|
+
)
|
|
401
|
+
""",
|
|
402
|
+
effective_user_id,
|
|
403
|
+
effective_user_name,
|
|
404
|
+
effective_user_email,
|
|
405
|
+
mode.value if mode else None,
|
|
406
|
+
page,
|
|
407
|
+
page_size,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Extract total from first row
|
|
411
|
+
total = rows[0]["total_count"] if rows else 0
|
|
412
|
+
|
|
413
|
+
# Convert rows to SessionWithUser
|
|
414
|
+
data = [
|
|
415
|
+
SessionWithUser(
|
|
416
|
+
id=str(row["id"]),
|
|
417
|
+
name=row["name"],
|
|
418
|
+
mode=row["mode"],
|
|
419
|
+
description=row["description"],
|
|
420
|
+
user_id=row["user_id"],
|
|
421
|
+
user_name=row["user_name"],
|
|
422
|
+
user_email=row["user_email"],
|
|
423
|
+
message_count=row["message_count"] or 0,
|
|
424
|
+
total_tokens=row["total_tokens"],
|
|
425
|
+
created_at=row["created_at"],
|
|
426
|
+
updated_at=row["updated_at"],
|
|
427
|
+
metadata=row["metadata"],
|
|
428
|
+
)
|
|
429
|
+
for row in rows
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
total_pages = (total + page_size - 1) // page_size if total > 0 else 0
|
|
433
|
+
|
|
434
|
+
return SessionsQueryResponse(
|
|
435
|
+
data=data,
|
|
436
|
+
metadata=PaginationMetadata(
|
|
437
|
+
total=total,
|
|
438
|
+
page=page,
|
|
439
|
+
page_size=page_size,
|
|
440
|
+
total_pages=total_pages,
|
|
441
|
+
has_next=page < total_pages,
|
|
442
|
+
has_previous=page > 1,
|
|
443
|
+
),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@router.post(
|
|
448
|
+
"/sessions",
|
|
449
|
+
response_model=Session,
|
|
450
|
+
status_code=201,
|
|
451
|
+
tags=["sessions"],
|
|
452
|
+
responses={
|
|
453
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
454
|
+
},
|
|
455
|
+
)
|
|
456
|
+
async def create_session(
|
|
457
|
+
request_body: SessionCreateRequest,
|
|
458
|
+
user: dict = Depends(require_admin),
|
|
459
|
+
x_user_id: str = Header(alias="X-User-Id", default="default"),
|
|
460
|
+
) -> Session:
|
|
461
|
+
"""
|
|
462
|
+
Create a new session.
|
|
463
|
+
|
|
464
|
+
**Requires admin role.**
|
|
465
|
+
|
|
466
|
+
For normal sessions, only name is required.
|
|
467
|
+
For evaluation sessions, you can specify:
|
|
468
|
+
- original_trace_id: The session being re-evaluated
|
|
469
|
+
- settings_overrides: Model, temperature, prompt overrides
|
|
470
|
+
- prompt: Custom prompt to test
|
|
471
|
+
|
|
472
|
+
Headers:
|
|
473
|
+
- X-User-Id: User identifier (owner of the session)
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Created session object
|
|
477
|
+
"""
|
|
478
|
+
if not settings.postgres.enabled:
|
|
479
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
480
|
+
|
|
481
|
+
# Admin can specify x_user_id, or default to their own
|
|
482
|
+
effective_user_id = x_user_id if x_user_id != "default" else user.get("id", "default")
|
|
483
|
+
|
|
484
|
+
session = Session(
|
|
485
|
+
name=request_body.name,
|
|
486
|
+
mode=request_body.mode,
|
|
487
|
+
description=request_body.description,
|
|
488
|
+
original_trace_id=request_body.original_trace_id,
|
|
489
|
+
settings_overrides=request_body.settings_overrides,
|
|
490
|
+
prompt=request_body.prompt,
|
|
491
|
+
agent_schema_uri=request_body.agent_schema_uri,
|
|
492
|
+
user_id=effective_user_id,
|
|
493
|
+
tenant_id="default", # tenant_id not used for filtering, set to default
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
repo = Repository(Session, table_name="sessions")
|
|
497
|
+
result = await repo.upsert(session)
|
|
498
|
+
|
|
499
|
+
logger.info(
|
|
500
|
+
f"Admin {user.get('email')} created session '{session.name}' "
|
|
501
|
+
f"(mode={session.mode}) for user={effective_user_id}"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return result # type: ignore
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@router.get(
|
|
508
|
+
"/sessions/{session_id}",
|
|
509
|
+
response_model=Session,
|
|
510
|
+
tags=["sessions"],
|
|
511
|
+
responses={
|
|
512
|
+
403: {"model": ErrorResponse, "description": "Access denied: not owner"},
|
|
513
|
+
404: {"model": ErrorResponse, "description": "Session not found"},
|
|
514
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
515
|
+
},
|
|
516
|
+
)
|
|
517
|
+
async def get_session(
|
|
518
|
+
request: Request,
|
|
519
|
+
session_id: str,
|
|
520
|
+
) -> Session:
|
|
521
|
+
"""
|
|
522
|
+
Get a specific session by ID.
|
|
523
|
+
|
|
524
|
+
Access Control:
|
|
525
|
+
- Regular users: Only access their own sessions
|
|
526
|
+
- Admin users: Can access any session
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
session_id: UUID of the session
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
Session object if found
|
|
533
|
+
|
|
534
|
+
Raises:
|
|
535
|
+
404: Session not found
|
|
536
|
+
403: Access denied (not owner and not admin)
|
|
537
|
+
"""
|
|
538
|
+
if not settings.postgres.enabled:
|
|
539
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
540
|
+
|
|
541
|
+
repo = Repository(Session, table_name="sessions")
|
|
542
|
+
session = await repo.get_by_id(session_id)
|
|
543
|
+
|
|
544
|
+
if not session:
|
|
545
|
+
raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
|
|
546
|
+
|
|
547
|
+
# Check access: admin or owner
|
|
548
|
+
current_user = get_current_user(request)
|
|
549
|
+
if not is_admin(current_user):
|
|
550
|
+
user_id = current_user.get("id") if current_user else None
|
|
551
|
+
if session.user_id and session.user_id != user_id:
|
|
552
|
+
raise HTTPException(status_code=403, detail="Access denied: not owner")
|
|
553
|
+
|
|
554
|
+
return session
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@router.put(
|
|
558
|
+
"/sessions/{session_id}",
|
|
559
|
+
response_model=Session,
|
|
560
|
+
tags=["sessions"],
|
|
561
|
+
responses={
|
|
562
|
+
403: {"model": ErrorResponse, "description": "Access denied: not owner"},
|
|
563
|
+
404: {"model": ErrorResponse, "description": "Session not found"},
|
|
564
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
565
|
+
},
|
|
566
|
+
)
|
|
567
|
+
async def update_session(
|
|
568
|
+
request: Request,
|
|
569
|
+
session_id: str,
|
|
570
|
+
request_body: SessionUpdateRequest,
|
|
571
|
+
) -> Session:
|
|
572
|
+
"""
|
|
573
|
+
Update an existing session.
|
|
574
|
+
|
|
575
|
+
Access Control:
|
|
576
|
+
- Regular users: Only update their own sessions
|
|
577
|
+
- Admin users: Can update any session
|
|
578
|
+
|
|
579
|
+
Allows updating:
|
|
580
|
+
- description
|
|
581
|
+
- settings_overrides
|
|
582
|
+
- prompt
|
|
583
|
+
- message_count (typically updated automatically)
|
|
584
|
+
- total_tokens (typically updated automatically)
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
session_id: UUID of the session
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Updated session object
|
|
591
|
+
|
|
592
|
+
Raises:
|
|
593
|
+
404: Session not found
|
|
594
|
+
403: Access denied (not owner and not admin)
|
|
595
|
+
"""
|
|
596
|
+
if not settings.postgres.enabled:
|
|
597
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
598
|
+
|
|
599
|
+
repo = Repository(Session, table_name="sessions")
|
|
600
|
+
session = await repo.get_by_id(session_id)
|
|
601
|
+
|
|
602
|
+
if not session:
|
|
603
|
+
raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
|
|
604
|
+
|
|
605
|
+
# Check access: admin or owner
|
|
606
|
+
current_user = get_current_user(request)
|
|
607
|
+
if not is_admin(current_user):
|
|
608
|
+
user_id = current_user.get("id") if current_user else None
|
|
609
|
+
if session.user_id and session.user_id != user_id:
|
|
610
|
+
raise HTTPException(status_code=403, detail="Access denied: not owner")
|
|
611
|
+
|
|
612
|
+
# Apply updates
|
|
613
|
+
update_data = request_body.model_dump(exclude_none=True)
|
|
614
|
+
for field, value in update_data.items():
|
|
615
|
+
setattr(session, field, value)
|
|
616
|
+
|
|
617
|
+
session.updated_at = utc_now()
|
|
618
|
+
|
|
619
|
+
result = await repo.update(session)
|
|
620
|
+
return result
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Models endpoint - List available LLM models.
|
|
3
|
+
|
|
4
|
+
Provides an OpenAI-compatible /v1/models endpoint listing all supported
|
|
5
|
+
LLM providers and their models using the provider:model_id syntax.
|
|
6
|
+
|
|
7
|
+
Endpoint:
|
|
8
|
+
GET /api/v1/models - List all available models
|
|
9
|
+
|
|
10
|
+
Response format matches OpenAI API for drop-in compatibility.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter, HTTPException
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
from .common import ErrorResponse
|
|
19
|
+
|
|
20
|
+
from rem.agentic.llm_provider_models import (
|
|
21
|
+
ModelInfo,
|
|
22
|
+
AVAILABLE_MODELS,
|
|
23
|
+
ALLOWED_MODEL_IDS,
|
|
24
|
+
is_valid_model,
|
|
25
|
+
get_valid_model_or_default,
|
|
26
|
+
get_model_by_id,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
router = APIRouter(prefix="/api/v1", tags=["models"])
|
|
30
|
+
|
|
31
|
+
# Re-export for backwards compatibility
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ModelInfo",
|
|
34
|
+
"AVAILABLE_MODELS",
|
|
35
|
+
"ALLOWED_MODEL_IDS",
|
|
36
|
+
"is_valid_model",
|
|
37
|
+
"get_valid_model_or_default",
|
|
38
|
+
"get_model_by_id",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ModelsResponse(BaseModel):
|
|
43
|
+
"""Response from /models endpoint."""
|
|
44
|
+
|
|
45
|
+
object: Literal["list"] = "list"
|
|
46
|
+
data: list[ModelInfo]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.get("/models", response_model=ModelsResponse)
|
|
50
|
+
async def list_models() -> ModelsResponse:
|
|
51
|
+
"""
|
|
52
|
+
List all available LLM models.
|
|
53
|
+
|
|
54
|
+
Returns models from all supported providers (OpenAI, Anthropic, Google, Cerebras)
|
|
55
|
+
with the provider:model_id naming convention.
|
|
56
|
+
|
|
57
|
+
Response format is OpenAI-compatible for drop-in replacement.
|
|
58
|
+
"""
|
|
59
|
+
return ModelsResponse(data=AVAILABLE_MODELS)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.get(
|
|
63
|
+
"/models/{model_id:path}",
|
|
64
|
+
response_model=ModelInfo,
|
|
65
|
+
responses={
|
|
66
|
+
404: {"model": ErrorResponse, "description": "Model not found"},
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
async def get_model(model_id: str) -> ModelInfo:
|
|
70
|
+
"""
|
|
71
|
+
Get information about a specific model.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
model_id: Model identifier in provider:model format (e.g., "openai:gpt-4.1")
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Model information if found
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
HTTPException: 404 if model not found
|
|
81
|
+
"""
|
|
82
|
+
model = get_model_by_id(model_id)
|
|
83
|
+
if model:
|
|
84
|
+
return model
|
|
85
|
+
|
|
86
|
+
raise HTTPException(status_code=404, detail=f"Model '{model_id}' not found")
|