remdb 0.3.103__py3-none-any.whl → 0.3.141__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/agentic/agents/sse_simulator.py +2 -0
- rem/agentic/context.py +51 -27
- rem/agentic/mcp/tool_wrapper.py +155 -18
- rem/agentic/otel/setup.py +93 -4
- rem/agentic/providers/phoenix.py +371 -108
- rem/agentic/providers/pydantic_ai.py +195 -46
- rem/agentic/schema.py +361 -21
- rem/agentic/tools/rem_tools.py +3 -3
- rem/api/main.py +85 -16
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +18 -4
- rem/api/mcp_router/tools.py +394 -16
- rem/api/routers/admin.py +218 -1
- rem/api/routers/chat/completions.py +280 -7
- rem/api/routers/chat/models.py +81 -7
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +17 -1
- rem/api/routers/chat/streaming.py +177 -3
- rem/api/routers/feedback.py +142 -329
- rem/api/routers/query.py +360 -0
- rem/api/routers/shared_sessions.py +13 -13
- rem/cli/commands/README.md +237 -64
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +4 -7
- rem/cli/commands/db.py +354 -143
- rem/cli/commands/experiments.py +436 -30
- rem/cli/commands/process.py +14 -8
- rem/cli/commands/schema.py +92 -45
- rem/cli/commands/session.py +336 -0
- rem/cli/dreaming.py +2 -2
- rem/cli/main.py +29 -6
- rem/config.py +8 -1
- rem/models/core/experiment.py +54 -0
- rem/models/core/rem_query.py +5 -2
- rem/models/entities/ontology.py +1 -1
- rem/models/entities/ontology_config.py +1 -1
- rem/models/entities/shared_session.py +2 -28
- rem/registry.py +10 -4
- rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
- rem/schemas/agents/examples/contract-extractor.yaml +1 -1
- rem/schemas/agents/examples/cv-parser.yaml +1 -1
- rem/services/content/service.py +30 -8
- rem/services/embeddings/api.py +4 -4
- rem/services/embeddings/worker.py +16 -16
- rem/services/phoenix/client.py +59 -18
- rem/services/postgres/README.md +151 -26
- rem/services/postgres/__init__.py +2 -1
- rem/services/postgres/diff_service.py +531 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
- rem/services/postgres/schema_generator.py +205 -4
- rem/services/postgres/service.py +6 -6
- rem/services/rem/parser.py +44 -9
- rem/services/rem/service.py +36 -2
- rem/services/session/compression.py +7 -0
- rem/services/session/reload.py +1 -1
- rem/settings.py +288 -16
- rem/sql/background_indexes.sql +19 -24
- rem/sql/migrations/001_install.sql +252 -69
- rem/sql/migrations/002_install_models.sql +2197 -619
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/utils/__init__.py +18 -0
- rem/utils/date_utils.py +2 -2
- rem/utils/schema_loader.py +110 -15
- rem/utils/sql_paths.py +146 -0
- rem/utils/vision.py +1 -1
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.103.dist-info → remdb-0.3.141.dist-info}/METADATA +300 -215
- {remdb-0.3.103.dist-info → remdb-0.3.141.dist-info}/RECORD +73 -64
- rem/sql/migrations/003_seed_default_user.sql +0 -48
- {remdb-0.3.103.dist-info → remdb-0.3.141.dist-info}/WHEEL +0 -0
- {remdb-0.3.103.dist-info → remdb-0.3.141.dist-info}/entry_points.txt +0 -0
rem/api/routers/feedback.py
CHANGED
|
@@ -1,35 +1,74 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Message feedback endpoint.
|
|
3
3
|
|
|
4
|
-
Provides
|
|
5
|
-
- Submitting feedback on messages or sessions
|
|
6
|
-
- Listing feedback with filters
|
|
7
|
-
- Syncing feedback to Phoenix as annotations (async)
|
|
4
|
+
Provides endpoint for submitting feedback on messages.
|
|
8
5
|
|
|
9
6
|
Endpoints:
|
|
10
|
-
POST /api/v1/feedback
|
|
11
|
-
GET /api/v1/feedback - List feedback with filters
|
|
12
|
-
GET /api/v1/feedback/{id} - Get specific feedback
|
|
13
|
-
GET /api/v1/feedback/categories - List available categories
|
|
7
|
+
POST /api/v1/messages/feedback - Submit feedback on a message
|
|
14
8
|
|
|
15
9
|
Trace Integration:
|
|
16
|
-
- Feedback
|
|
17
|
-
- Phoenix sync attaches feedback as span annotations
|
|
10
|
+
- Feedback auto-resolves trace_id/span_id from the message in the database
|
|
11
|
+
- Phoenix sync attaches feedback as span annotations when trace info is available
|
|
12
|
+
|
|
13
|
+
HTTP Status Codes:
|
|
14
|
+
- 201: Feedback saved AND synced to Phoenix as annotation (phoenix_synced=true)
|
|
15
|
+
- 200: Feedback accepted and saved to DB, but NOT synced to Phoenix
|
|
16
|
+
(missing trace_id/span_id, Phoenix disabled, or sync failed)
|
|
17
|
+
|
|
18
|
+
IMPORTANT - Testing Requirements:
|
|
19
|
+
╔════════════════════════════════════════════════════════════════════════════════════════════════════╗
|
|
20
|
+
║ 1. Use 'rem' agent (NOT 'simulator') - only real agents capture traces ║
|
|
21
|
+
║ 2. Session IDs MUST be UUIDs - use python3 -c "import uuid; print(uuid.uuid4())" ║
|
|
22
|
+
║ 3. Port-forward OTEL collector: kubectl port-forward -n observability ║
|
|
23
|
+
║ svc/otel-collector-collector 4318:4318 ║
|
|
24
|
+
║ 4. Port-forward Phoenix: kubectl port-forward -n siggy svc/phoenix 6006:6006 ║
|
|
25
|
+
║ 5. Set environment variables when starting the API: ║
|
|
26
|
+
║ OTEL__ENABLED=true PHOENIX__ENABLED=true PHOENIX_API_KEY=<jwt> uvicorn ... ║
|
|
27
|
+
║ 6. Get PHOENIX_API_KEY: ║
|
|
28
|
+
║ kubectl get secret -n siggy rem-phoenix-api-key -o jsonpath='{.data.PHOENIX_API_KEY}' ║
|
|
29
|
+
║ | base64 -d ║
|
|
30
|
+
╚════════════════════════════════════════════════════════════════════════════════════════════════════╝
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
# 1. Send a chat message with X-Session-Id header (MUST be UUID!)
|
|
34
|
+
SESSION_ID=$(python3 -c "import uuid; print(uuid.uuid4())")
|
|
35
|
+
curl -X POST http://localhost:8000/api/v1/chat/completions \\
|
|
36
|
+
-H "Content-Type: application/json" \\
|
|
37
|
+
-H "X-Session-Id: $SESSION_ID" \\
|
|
38
|
+
-H "X-Agent-Schema: rem" \\
|
|
39
|
+
-d '{"messages": [{"role": "user", "content": "hello"}], "stream": true}'
|
|
40
|
+
|
|
41
|
+
# 2. Extract message_id from the 'metadata' SSE event:
|
|
42
|
+
# event: metadata
|
|
43
|
+
# data: {"message_id": "728882f8-...", "trace_id": "e53c701c...", ...}
|
|
44
|
+
|
|
45
|
+
# 3. Submit feedback referencing that message (trace_id auto-resolved from DB)
|
|
46
|
+
curl -X POST http://localhost:8000/api/v1/messages/feedback \\
|
|
47
|
+
-H "Content-Type: application/json" \\
|
|
48
|
+
-H "X-Tenant-Id: default" \\
|
|
49
|
+
-d '{
|
|
50
|
+
"session_id": "'$SESSION_ID'",
|
|
51
|
+
"message_id": "<message-id-from-metadata>",
|
|
52
|
+
"rating": 1,
|
|
53
|
+
"categories": ["helpful"],
|
|
54
|
+
"comment": "Great response!"
|
|
55
|
+
}'
|
|
56
|
+
|
|
57
|
+
# 4. Check response:
|
|
58
|
+
# - 201 + phoenix_synced=true = annotation synced to Phoenix (check Phoenix UI at :6006)
|
|
59
|
+
# - 200 + phoenix_synced=false = feedback saved but not synced (missing trace info)
|
|
18
60
|
"""
|
|
19
61
|
|
|
20
|
-
from
|
|
21
|
-
|
|
22
|
-
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
|
|
62
|
+
from fastapi import APIRouter, Header, HTTPException, Request, Response
|
|
23
63
|
from loguru import logger
|
|
24
64
|
from pydantic import BaseModel, Field
|
|
25
65
|
|
|
26
|
-
from ..deps import
|
|
27
|
-
from ...models.entities import Feedback
|
|
66
|
+
from ..deps import get_user_id_from_request
|
|
67
|
+
from ...models.entities import Feedback
|
|
28
68
|
from ...services.postgres import Repository
|
|
29
69
|
from ...settings import settings
|
|
30
|
-
from ...utils.date_utils import utc_now
|
|
31
70
|
|
|
32
|
-
router = APIRouter(prefix="/api/v1", tags=["
|
|
71
|
+
router = APIRouter(prefix="/api/v1", tags=["messages"])
|
|
33
72
|
|
|
34
73
|
|
|
35
74
|
# =============================================================================
|
|
@@ -51,10 +90,9 @@ class FeedbackCreateRequest(BaseModel):
|
|
|
51
90
|
description="Rating: -1 (thumbs down), 1 (thumbs up), or 1-5 scale",
|
|
52
91
|
)
|
|
53
92
|
categories: list[str] = Field(
|
|
54
|
-
default_factory=list, description="
|
|
93
|
+
default_factory=list, description="Feedback categories"
|
|
55
94
|
)
|
|
56
95
|
comment: str | None = Field(default=None, description="Free-text comment")
|
|
57
|
-
# Optional trace reference (can be auto-resolved from message)
|
|
58
96
|
trace_id: str | None = Field(
|
|
59
97
|
default=None, description="OTEL trace ID (auto-resolved if message has it)"
|
|
60
98
|
)
|
|
@@ -78,134 +116,15 @@ class FeedbackResponse(BaseModel):
|
|
|
78
116
|
created_at: str
|
|
79
117
|
|
|
80
118
|
|
|
81
|
-
class FeedbackListResponse(BaseModel):
|
|
82
|
-
"""Response for feedback list endpoint."""
|
|
83
|
-
|
|
84
|
-
object: Literal["list"] = "list"
|
|
85
|
-
data: list[Feedback]
|
|
86
|
-
total: int
|
|
87
|
-
has_more: bool
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class CategoryInfo(BaseModel):
|
|
91
|
-
"""Information about a feedback category."""
|
|
92
|
-
|
|
93
|
-
value: str
|
|
94
|
-
label: str
|
|
95
|
-
description: str
|
|
96
|
-
sentiment: Literal["positive", "negative", "neutral"]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
class CategoriesResponse(BaseModel):
|
|
100
|
-
"""Response for categories endpoint."""
|
|
101
|
-
|
|
102
|
-
categories: list[CategoryInfo]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# =============================================================================
|
|
106
|
-
# Category Definitions
|
|
107
|
-
# =============================================================================
|
|
108
|
-
|
|
109
|
-
CATEGORY_INFO: dict[str, CategoryInfo] = {
|
|
110
|
-
FeedbackCategory.INCOMPLETE.value: CategoryInfo(
|
|
111
|
-
value=FeedbackCategory.INCOMPLETE.value,
|
|
112
|
-
label="Incomplete",
|
|
113
|
-
description="Response lacks expected information",
|
|
114
|
-
sentiment="negative",
|
|
115
|
-
),
|
|
116
|
-
FeedbackCategory.INACCURATE.value: CategoryInfo(
|
|
117
|
-
value=FeedbackCategory.INACCURATE.value,
|
|
118
|
-
label="Inaccurate",
|
|
119
|
-
description="Response contains factual errors",
|
|
120
|
-
sentiment="negative",
|
|
121
|
-
),
|
|
122
|
-
FeedbackCategory.POOR_TONE.value: CategoryInfo(
|
|
123
|
-
value=FeedbackCategory.POOR_TONE.value,
|
|
124
|
-
label="Poor Tone",
|
|
125
|
-
description="Inappropriate or unprofessional tone",
|
|
126
|
-
sentiment="negative",
|
|
127
|
-
),
|
|
128
|
-
FeedbackCategory.OFF_TOPIC.value: CategoryInfo(
|
|
129
|
-
value=FeedbackCategory.OFF_TOPIC.value,
|
|
130
|
-
label="Off Topic",
|
|
131
|
-
description="Response doesn't address the question",
|
|
132
|
-
sentiment="negative",
|
|
133
|
-
),
|
|
134
|
-
FeedbackCategory.TOO_VERBOSE.value: CategoryInfo(
|
|
135
|
-
value=FeedbackCategory.TOO_VERBOSE.value,
|
|
136
|
-
label="Too Verbose",
|
|
137
|
-
description="Unnecessarily long response",
|
|
138
|
-
sentiment="negative",
|
|
139
|
-
),
|
|
140
|
-
FeedbackCategory.TOO_BRIEF.value: CategoryInfo(
|
|
141
|
-
value=FeedbackCategory.TOO_BRIEF.value,
|
|
142
|
-
label="Too Brief",
|
|
143
|
-
description="Insufficiently detailed response",
|
|
144
|
-
sentiment="negative",
|
|
145
|
-
),
|
|
146
|
-
FeedbackCategory.CONFUSING.value: CategoryInfo(
|
|
147
|
-
value=FeedbackCategory.CONFUSING.value,
|
|
148
|
-
label="Confusing",
|
|
149
|
-
description="Hard to understand or unclear",
|
|
150
|
-
sentiment="negative",
|
|
151
|
-
),
|
|
152
|
-
FeedbackCategory.UNSAFE.value: CategoryInfo(
|
|
153
|
-
value=FeedbackCategory.UNSAFE.value,
|
|
154
|
-
label="Unsafe",
|
|
155
|
-
description="Contains potentially harmful content",
|
|
156
|
-
sentiment="negative",
|
|
157
|
-
),
|
|
158
|
-
FeedbackCategory.HELPFUL.value: CategoryInfo(
|
|
159
|
-
value=FeedbackCategory.HELPFUL.value,
|
|
160
|
-
label="Helpful",
|
|
161
|
-
description="Response was useful and addressed the need",
|
|
162
|
-
sentiment="positive",
|
|
163
|
-
),
|
|
164
|
-
FeedbackCategory.EXCELLENT.value: CategoryInfo(
|
|
165
|
-
value=FeedbackCategory.EXCELLENT.value,
|
|
166
|
-
label="Excellent",
|
|
167
|
-
description="Exceptionally good response",
|
|
168
|
-
sentiment="positive",
|
|
169
|
-
),
|
|
170
|
-
FeedbackCategory.ACCURATE.value: CategoryInfo(
|
|
171
|
-
value=FeedbackCategory.ACCURATE.value,
|
|
172
|
-
label="Accurate",
|
|
173
|
-
description="Factually correct and precise",
|
|
174
|
-
sentiment="positive",
|
|
175
|
-
),
|
|
176
|
-
FeedbackCategory.WELL_WRITTEN.value: CategoryInfo(
|
|
177
|
-
value=FeedbackCategory.WELL_WRITTEN.value,
|
|
178
|
-
label="Well Written",
|
|
179
|
-
description="Clear, well-structured response",
|
|
180
|
-
sentiment="positive",
|
|
181
|
-
),
|
|
182
|
-
FeedbackCategory.OTHER.value: CategoryInfo(
|
|
183
|
-
value=FeedbackCategory.OTHER.value,
|
|
184
|
-
label="Other",
|
|
185
|
-
description="Other feedback not covered by categories",
|
|
186
|
-
sentiment="neutral",
|
|
187
|
-
),
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
119
|
# =============================================================================
|
|
192
|
-
# Feedback
|
|
120
|
+
# Feedback Endpoint
|
|
193
121
|
# =============================================================================
|
|
194
122
|
|
|
195
123
|
|
|
196
|
-
@router.
|
|
197
|
-
async def list_categories() -> CategoriesResponse:
|
|
198
|
-
"""
|
|
199
|
-
List available feedback categories.
|
|
200
|
-
|
|
201
|
-
Returns predefined categories with labels, descriptions, and sentiment.
|
|
202
|
-
"""
|
|
203
|
-
return CategoriesResponse(categories=list(CATEGORY_INFO.values()))
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
@router.post("/feedback", response_model=FeedbackResponse, status_code=201)
|
|
124
|
+
@router.post("/messages/feedback", response_model=FeedbackResponse)
|
|
207
125
|
async def submit_feedback(
|
|
208
126
|
request: Request,
|
|
127
|
+
response: Response,
|
|
209
128
|
request_body: FeedbackCreateRequest,
|
|
210
129
|
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
211
130
|
) -> FeedbackResponse:
|
|
@@ -219,15 +138,16 @@ async def submit_feedback(
|
|
|
219
138
|
- Provided explicitly in the request
|
|
220
139
|
- Auto-resolved from the message if message_id is provided
|
|
221
140
|
|
|
222
|
-
|
|
141
|
+
HTTP Status Codes:
|
|
142
|
+
- 201: Feedback saved AND synced to Phoenix (phoenix_synced=true)
|
|
143
|
+
- 200: Feedback accepted but NOT synced (missing trace info, disabled, or failed)
|
|
223
144
|
|
|
224
145
|
Returns:
|
|
225
|
-
Created feedback object
|
|
146
|
+
Created feedback object with phoenix_synced indicating sync status
|
|
226
147
|
"""
|
|
227
148
|
if not settings.postgres.enabled:
|
|
228
149
|
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
229
150
|
|
|
230
|
-
# Get effective user_id from auth or anonymous tracking
|
|
231
151
|
effective_user_id = get_user_id_from_request(request)
|
|
232
152
|
|
|
233
153
|
# Resolve trace_id/span_id from message if not provided
|
|
@@ -235,14 +155,45 @@ async def submit_feedback(
|
|
|
235
155
|
span_id = request_body.span_id
|
|
236
156
|
|
|
237
157
|
if request_body.message_id and (not trace_id or not span_id):
|
|
238
|
-
#
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
158
|
+
# Look up message by ID to get trace context
|
|
159
|
+
# Note: Messages are stored with tenant_id=user_id (not x_tenant_id header)
|
|
160
|
+
# so we query by ID only - UUIDs are globally unique
|
|
161
|
+
from ...services.postgres import PostgresService
|
|
162
|
+
import uuid
|
|
163
|
+
|
|
164
|
+
logger.info(f"Looking up trace context for message_id={request_body.message_id}")
|
|
165
|
+
|
|
166
|
+
# Convert message_id string to UUID for database query
|
|
167
|
+
try:
|
|
168
|
+
message_uuid = uuid.UUID(request_body.message_id)
|
|
169
|
+
except ValueError as e:
|
|
170
|
+
logger.warning(f"Invalid message_id format '{request_body.message_id}': {e}")
|
|
171
|
+
message_uuid = None
|
|
172
|
+
|
|
173
|
+
if message_uuid:
|
|
174
|
+
db = PostgresService()
|
|
175
|
+
# Ensure connection (same pattern as Repository)
|
|
176
|
+
if not db.pool:
|
|
177
|
+
await db.connect()
|
|
178
|
+
|
|
179
|
+
if db.pool:
|
|
180
|
+
query = """
|
|
181
|
+
SELECT trace_id, span_id FROM messages
|
|
182
|
+
WHERE id = $1 AND deleted_at IS NULL
|
|
183
|
+
LIMIT 1
|
|
184
|
+
"""
|
|
185
|
+
async with db.pool.acquire() as conn:
|
|
186
|
+
row = await conn.fetchrow(query, message_uuid)
|
|
187
|
+
logger.info(f"Database query result for message {request_body.message_id}: row={row}")
|
|
188
|
+
if row:
|
|
189
|
+
trace_id = trace_id or row["trace_id"]
|
|
190
|
+
span_id = span_id or row["span_id"]
|
|
191
|
+
logger.info(f"Found trace context for message {request_body.message_id}: trace_id={trace_id}, span_id={span_id}")
|
|
192
|
+
else:
|
|
193
|
+
logger.warning(f"No message found in database with id={request_body.message_id}")
|
|
194
|
+
else:
|
|
195
|
+
logger.warning(f"Database pool not available for message lookup after connect attempt")
|
|
196
|
+
|
|
246
197
|
feedback = Feedback(
|
|
247
198
|
session_id=request_body.session_id,
|
|
248
199
|
message_id=request_body.message_id,
|
|
@@ -257,22 +208,51 @@ async def submit_feedback(
|
|
|
257
208
|
tenant_id=x_tenant_id,
|
|
258
209
|
)
|
|
259
210
|
|
|
260
|
-
# Store feedback (table is "feedbacks" - plural)
|
|
261
211
|
repo = Repository(Feedback, table_name="feedbacks")
|
|
262
212
|
result = await repo.upsert(feedback)
|
|
263
213
|
|
|
264
214
|
logger.info(
|
|
265
215
|
f"Feedback submitted: session={request_body.session_id}, "
|
|
266
|
-
f"message={request_body.message_id}, rating={request_body.rating}
|
|
267
|
-
f"categories={request_body.categories}"
|
|
216
|
+
f"message={request_body.message_id}, rating={request_body.rating}"
|
|
268
217
|
)
|
|
269
218
|
|
|
270
|
-
#
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
219
|
+
# Sync to Phoenix if trace_id/span_id available and Phoenix is enabled
|
|
220
|
+
phoenix_synced = False
|
|
221
|
+
phoenix_annotation_id = None
|
|
222
|
+
|
|
223
|
+
if trace_id and span_id and settings.phoenix.enabled:
|
|
224
|
+
try:
|
|
225
|
+
from ...services.phoenix import PhoenixClient
|
|
226
|
+
|
|
227
|
+
phoenix_client = PhoenixClient()
|
|
228
|
+
phoenix_annotation_id = phoenix_client.sync_user_feedback(
|
|
229
|
+
span_id=span_id,
|
|
230
|
+
rating=request_body.rating,
|
|
231
|
+
categories=request_body.categories,
|
|
232
|
+
comment=request_body.comment,
|
|
233
|
+
feedback_id=str(result.id),
|
|
234
|
+
trace_id=trace_id,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if phoenix_annotation_id:
|
|
238
|
+
phoenix_synced = True
|
|
239
|
+
# Update the feedback record with sync status
|
|
240
|
+
result.phoenix_synced = True
|
|
241
|
+
result.phoenix_annotation_id = phoenix_annotation_id
|
|
242
|
+
await repo.upsert(result)
|
|
243
|
+
logger.info(f"Feedback synced to Phoenix: annotation_id={phoenix_annotation_id}")
|
|
244
|
+
else:
|
|
245
|
+
logger.warning(f"Phoenix sync returned no annotation ID for feedback {result.id}")
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error(f"Failed to sync feedback to Phoenix: {e}")
|
|
249
|
+
# Don't fail the request if Phoenix sync fails
|
|
250
|
+
elif trace_id and span_id:
|
|
251
|
+
logger.debug(f"Feedback has trace info but Phoenix disabled: trace={trace_id}, span={span_id}")
|
|
252
|
+
|
|
253
|
+
# Set HTTP status code based on Phoenix sync result
|
|
254
|
+
# 201 = synced to Phoenix, 200 = accepted but not synced
|
|
255
|
+
response.status_code = 201 if phoenix_synced else 200
|
|
276
256
|
|
|
277
257
|
return FeedbackResponse(
|
|
278
258
|
id=str(result.id),
|
|
@@ -286,170 +266,3 @@ async def submit_feedback(
|
|
|
286
266
|
phoenix_synced=result.phoenix_synced,
|
|
287
267
|
created_at=result.created_at.isoformat() if result.created_at else "",
|
|
288
268
|
)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
@router.get("/feedback", response_model=FeedbackListResponse)
|
|
292
|
-
async def list_feedback(
|
|
293
|
-
request: Request,
|
|
294
|
-
session_id: str | None = Query(default=None, description="Filter by session ID"),
|
|
295
|
-
message_id: str | None = Query(default=None, description="Filter by message ID"),
|
|
296
|
-
rating: int | None = Query(default=None, description="Filter by rating"),
|
|
297
|
-
category: str | None = Query(default=None, description="Filter by category"),
|
|
298
|
-
phoenix_synced: bool | None = Query(
|
|
299
|
-
default=None, description="Filter by Phoenix sync status"
|
|
300
|
-
),
|
|
301
|
-
limit: int = Query(default=50, ge=1, le=100, description="Max results"),
|
|
302
|
-
offset: int = Query(default=0, ge=0, description="Offset for pagination"),
|
|
303
|
-
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
304
|
-
) -> FeedbackListResponse:
|
|
305
|
-
"""
|
|
306
|
-
List feedback with optional filters.
|
|
307
|
-
|
|
308
|
-
Access Control:
|
|
309
|
-
- Regular users: Only see feedback they submitted
|
|
310
|
-
- Admin users: Can see all feedback
|
|
311
|
-
|
|
312
|
-
Filters:
|
|
313
|
-
- session_id: Filter by session
|
|
314
|
-
- message_id: Filter by specific message
|
|
315
|
-
- rating: Filter by rating value
|
|
316
|
-
- category: Filter by category (checks if category in list)
|
|
317
|
-
- phoenix_synced: Filter by sync status
|
|
318
|
-
"""
|
|
319
|
-
if not settings.postgres.enabled:
|
|
320
|
-
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
321
|
-
|
|
322
|
-
repo = Repository(Feedback, table_name="feedbacks")
|
|
323
|
-
|
|
324
|
-
# Build user-scoped filters (uses anon_id for anonymous users)
|
|
325
|
-
filters = await get_user_filter(request, x_tenant_id=x_tenant_id)
|
|
326
|
-
|
|
327
|
-
# Apply optional filters
|
|
328
|
-
if session_id:
|
|
329
|
-
filters["session_id"] = session_id
|
|
330
|
-
if message_id:
|
|
331
|
-
filters["message_id"] = message_id
|
|
332
|
-
if rating is not None:
|
|
333
|
-
filters["rating"] = rating
|
|
334
|
-
if phoenix_synced is not None:
|
|
335
|
-
filters["phoenix_synced"] = phoenix_synced
|
|
336
|
-
# TODO: category filter requires array contains query
|
|
337
|
-
|
|
338
|
-
feedback_list = await repo.find(
|
|
339
|
-
filters,
|
|
340
|
-
order_by="created_at DESC",
|
|
341
|
-
limit=limit + 1,
|
|
342
|
-
offset=offset,
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
# Filter by category in Python if specified (until Repository supports array contains)
|
|
346
|
-
if category:
|
|
347
|
-
feedback_list = [f for f in feedback_list if category in f.categories]
|
|
348
|
-
|
|
349
|
-
has_more = len(feedback_list) > limit
|
|
350
|
-
if has_more:
|
|
351
|
-
feedback_list = feedback_list[:limit]
|
|
352
|
-
|
|
353
|
-
total = await repo.count(filters)
|
|
354
|
-
|
|
355
|
-
return FeedbackListResponse(data=feedback_list, total=total, has_more=has_more)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
@router.get("/feedback/{feedback_id}", response_model=Feedback)
|
|
359
|
-
async def get_feedback(
|
|
360
|
-
request: Request,
|
|
361
|
-
feedback_id: str,
|
|
362
|
-
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
363
|
-
) -> Feedback:
|
|
364
|
-
"""
|
|
365
|
-
Get specific feedback by ID.
|
|
366
|
-
|
|
367
|
-
Access Control:
|
|
368
|
-
- Regular users: Only access their own feedback
|
|
369
|
-
- Admin users: Can access any feedback
|
|
370
|
-
"""
|
|
371
|
-
if not settings.postgres.enabled:
|
|
372
|
-
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
373
|
-
|
|
374
|
-
repo = Repository(Feedback, table_name="feedbacks")
|
|
375
|
-
feedback = await repo.get_by_id(feedback_id, x_tenant_id)
|
|
376
|
-
|
|
377
|
-
if not feedback:
|
|
378
|
-
raise HTTPException(status_code=404, detail=f"Feedback '{feedback_id}' not found")
|
|
379
|
-
|
|
380
|
-
# Check access
|
|
381
|
-
current_user = get_current_user(request)
|
|
382
|
-
if not is_admin(current_user):
|
|
383
|
-
user_id = current_user.get("id") if current_user else None
|
|
384
|
-
if feedback.user_id and feedback.user_id != user_id:
|
|
385
|
-
raise HTTPException(status_code=403, detail="Access denied: not owner")
|
|
386
|
-
|
|
387
|
-
return feedback
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
# =============================================================================
|
|
391
|
-
# Phoenix Sync (Stub - TODO: Implement background task)
|
|
392
|
-
# =============================================================================
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
async def sync_feedback_to_phoenix(feedback: Feedback) -> bool:
|
|
396
|
-
"""
|
|
397
|
-
Sync feedback to Phoenix as a span annotation.
|
|
398
|
-
|
|
399
|
-
TODO: Implement this as a background task.
|
|
400
|
-
|
|
401
|
-
This should:
|
|
402
|
-
1. Connect to Phoenix client
|
|
403
|
-
2. Resolve trace/span from feedback
|
|
404
|
-
3. Create annotation with feedback data
|
|
405
|
-
4. Update feedback.phoenix_synced = True
|
|
406
|
-
5. Store phoenix_annotation_id
|
|
407
|
-
|
|
408
|
-
Args:
|
|
409
|
-
feedback: Feedback entity to sync
|
|
410
|
-
|
|
411
|
-
Returns:
|
|
412
|
-
True if synced successfully
|
|
413
|
-
"""
|
|
414
|
-
if not feedback.span_id:
|
|
415
|
-
logger.warning(f"Cannot sync feedback {feedback.id}: no span_id")
|
|
416
|
-
return False
|
|
417
|
-
|
|
418
|
-
try:
|
|
419
|
-
# TODO: Import and use Phoenix client
|
|
420
|
-
# from ...services.phoenix import PhoenixClient
|
|
421
|
-
# client = PhoenixClient()
|
|
422
|
-
#
|
|
423
|
-
# # Build annotation from feedback
|
|
424
|
-
# label = None
|
|
425
|
-
# if feedback.categories:
|
|
426
|
-
# label = feedback.categories[0] # Primary category
|
|
427
|
-
#
|
|
428
|
-
# score = None
|
|
429
|
-
# if feedback.rating:
|
|
430
|
-
# # Normalize to 0-1 scale
|
|
431
|
-
# if feedback.rating == -1:
|
|
432
|
-
# score = 0.0
|
|
433
|
-
# elif feedback.rating >= 1 and feedback.rating <= 5:
|
|
434
|
-
# score = feedback.rating / 5.0
|
|
435
|
-
#
|
|
436
|
-
# client.add_span_feedback(
|
|
437
|
-
# span_id=feedback.span_id,
|
|
438
|
-
# annotation_name="user_feedback",
|
|
439
|
-
# annotator_kind=feedback.annotator_kind,
|
|
440
|
-
# label=label,
|
|
441
|
-
# score=score,
|
|
442
|
-
# explanation=feedback.comment,
|
|
443
|
-
# )
|
|
444
|
-
#
|
|
445
|
-
# # Update feedback record
|
|
446
|
-
# feedback.phoenix_synced = True
|
|
447
|
-
# repo = Repository(Feedback, table_name="feedbacks")
|
|
448
|
-
# await repo.update(feedback)
|
|
449
|
-
|
|
450
|
-
logger.info(f"TODO: Sync feedback {feedback.id} to Phoenix span {feedback.span_id}")
|
|
451
|
-
return True
|
|
452
|
-
|
|
453
|
-
except Exception as e:
|
|
454
|
-
logger.error(f"Failed to sync feedback to Phoenix: {e}")
|
|
455
|
-
return False
|