truthound-dashboard 1.0.2__py3-none-any.whl → 1.2.0__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.
- truthound_dashboard/api/catalog.py +343 -0
- truthound_dashboard/api/collaboration.py +148 -0
- truthound_dashboard/api/glossary.py +329 -0
- truthound_dashboard/api/router.py +29 -0
- truthound_dashboard/cli.py +397 -0
- truthound_dashboard/core/__init__.py +12 -0
- truthound_dashboard/core/phase5/__init__.py +17 -0
- truthound_dashboard/core/phase5/activity.py +144 -0
- truthound_dashboard/core/phase5/catalog.py +868 -0
- truthound_dashboard/core/phase5/collaboration.py +305 -0
- truthound_dashboard/core/phase5/glossary.py +828 -0
- truthound_dashboard/db/__init__.py +37 -0
- truthound_dashboard/db/models.py +693 -0
- truthound_dashboard/schemas/__init__.py +114 -0
- truthound_dashboard/schemas/catalog.py +352 -0
- truthound_dashboard/schemas/collaboration.py +169 -0
- truthound_dashboard/schemas/glossary.py +349 -0
- truthound_dashboard/translate/__init__.py +61 -0
- truthound_dashboard/translate/config_updater.py +327 -0
- truthound_dashboard/translate/exceptions.py +98 -0
- truthound_dashboard/translate/providers/__init__.py +49 -0
- truthound_dashboard/translate/providers/anthropic.py +135 -0
- truthound_dashboard/translate/providers/base.py +225 -0
- truthound_dashboard/translate/providers/mistral.py +138 -0
- truthound_dashboard/translate/providers/ollama.py +226 -0
- truthound_dashboard/translate/providers/openai.py +187 -0
- truthound_dashboard/translate/providers/registry.py +217 -0
- truthound_dashboard/translate/translator.py +443 -0
- {truthound_dashboard-1.0.2.dist-info → truthound_dashboard-1.2.0.dist-info}/METADATA +123 -4
- {truthound_dashboard-1.0.2.dist-info → truthound_dashboard-1.2.0.dist-info}/RECORD +33 -11
- {truthound_dashboard-1.0.2.dist-info → truthound_dashboard-1.2.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.0.2.dist-info → truthound_dashboard-1.2.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.0.2.dist-info → truthound_dashboard-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Collaboration service for Phase 5.
|
|
2
|
+
|
|
3
|
+
Provides business logic for managing comments and activity feeds.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from sqlalchemy import select
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from truthound_dashboard.db import (
|
|
14
|
+
Activity,
|
|
15
|
+
ActivityAction,
|
|
16
|
+
BaseRepository,
|
|
17
|
+
Comment,
|
|
18
|
+
ResourceType,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .activity import ActivityLogger, ActivityRepository
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Repositories
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommentRepository(BaseRepository[Comment]):
|
|
30
|
+
"""Repository for Comment model operations."""
|
|
31
|
+
|
|
32
|
+
model = Comment
|
|
33
|
+
|
|
34
|
+
async def get_for_resource(
|
|
35
|
+
self,
|
|
36
|
+
resource_type: str,
|
|
37
|
+
resource_id: str,
|
|
38
|
+
*,
|
|
39
|
+
limit: int = 100,
|
|
40
|
+
) -> list[Comment]:
|
|
41
|
+
"""Get comments for a resource.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
resource_type: Type of resource.
|
|
45
|
+
resource_id: Resource ID.
|
|
46
|
+
limit: Maximum to return.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of root comments (with replies loaded).
|
|
50
|
+
"""
|
|
51
|
+
result = await self.session.execute(
|
|
52
|
+
select(Comment)
|
|
53
|
+
.where(Comment.resource_type == resource_type)
|
|
54
|
+
.where(Comment.resource_id == resource_id)
|
|
55
|
+
.where(Comment.parent_id.is_(None)) # Only root comments
|
|
56
|
+
.order_by(Comment.created_at.desc())
|
|
57
|
+
.limit(limit)
|
|
58
|
+
)
|
|
59
|
+
return list(result.scalars().all())
|
|
60
|
+
|
|
61
|
+
async def count_for_resource(
|
|
62
|
+
self,
|
|
63
|
+
resource_type: str,
|
|
64
|
+
resource_id: str,
|
|
65
|
+
) -> int:
|
|
66
|
+
"""Count comments for a resource.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
resource_type: Type of resource.
|
|
70
|
+
resource_id: Resource ID.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Total count including replies.
|
|
74
|
+
"""
|
|
75
|
+
return await self.count([
|
|
76
|
+
Comment.resource_type == resource_type,
|
|
77
|
+
Comment.resource_id == resource_id,
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# =============================================================================
|
|
82
|
+
# Service
|
|
83
|
+
# =============================================================================
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CollaborationService:
|
|
87
|
+
"""Service for managing collaboration features.
|
|
88
|
+
|
|
89
|
+
Handles comments and activity feeds.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
93
|
+
"""Initialize service.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
session: Database session.
|
|
97
|
+
"""
|
|
98
|
+
self.session = session
|
|
99
|
+
self.comment_repo = CommentRepository(session)
|
|
100
|
+
self.activity_repo = ActivityRepository(session)
|
|
101
|
+
self.activity_logger = ActivityLogger(session)
|
|
102
|
+
|
|
103
|
+
# =========================================================================
|
|
104
|
+
# Comment Operations
|
|
105
|
+
# =========================================================================
|
|
106
|
+
|
|
107
|
+
async def get_comments(
|
|
108
|
+
self,
|
|
109
|
+
resource_type: str,
|
|
110
|
+
resource_id: str,
|
|
111
|
+
*,
|
|
112
|
+
limit: int = 100,
|
|
113
|
+
) -> tuple[list[Comment], int]:
|
|
114
|
+
"""Get comments for a resource.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
resource_type: Type of resource.
|
|
118
|
+
resource_id: Resource ID.
|
|
119
|
+
limit: Maximum to return.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Tuple of (comments, total_count).
|
|
123
|
+
"""
|
|
124
|
+
comments = await self.comment_repo.get_for_resource(
|
|
125
|
+
resource_type,
|
|
126
|
+
resource_id,
|
|
127
|
+
limit=limit,
|
|
128
|
+
)
|
|
129
|
+
total = await self.comment_repo.count_for_resource(
|
|
130
|
+
resource_type,
|
|
131
|
+
resource_id,
|
|
132
|
+
)
|
|
133
|
+
return comments, total
|
|
134
|
+
|
|
135
|
+
async def get_comment(self, comment_id: str) -> Comment | None:
|
|
136
|
+
"""Get comment by ID.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
comment_id: Comment ID.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Comment or None.
|
|
143
|
+
"""
|
|
144
|
+
return await self.comment_repo.get_by_id(comment_id)
|
|
145
|
+
|
|
146
|
+
async def create_comment(
|
|
147
|
+
self,
|
|
148
|
+
*,
|
|
149
|
+
resource_type: str,
|
|
150
|
+
resource_id: str,
|
|
151
|
+
content: str,
|
|
152
|
+
author_id: str | None = None,
|
|
153
|
+
parent_id: str | None = None,
|
|
154
|
+
) -> Comment:
|
|
155
|
+
"""Create a new comment.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
resource_type: Type of resource.
|
|
159
|
+
resource_id: Resource ID.
|
|
160
|
+
content: Comment content.
|
|
161
|
+
author_id: Author identifier.
|
|
162
|
+
parent_id: Parent comment ID for replies.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Created comment.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
ValueError: If parent comment not found or mismatched resource.
|
|
169
|
+
"""
|
|
170
|
+
# Validate parent if provided
|
|
171
|
+
if parent_id:
|
|
172
|
+
parent = await self.comment_repo.get_by_id(parent_id)
|
|
173
|
+
if not parent:
|
|
174
|
+
raise ValueError(f"Parent comment '{parent_id}' not found")
|
|
175
|
+
# Ensure reply is on same resource
|
|
176
|
+
if parent.resource_type != resource_type or parent.resource_id != resource_id:
|
|
177
|
+
raise ValueError("Reply must be on the same resource as parent")
|
|
178
|
+
|
|
179
|
+
comment = await self.comment_repo.create(
|
|
180
|
+
resource_type=resource_type,
|
|
181
|
+
resource_id=resource_id,
|
|
182
|
+
content=content,
|
|
183
|
+
author_id=author_id,
|
|
184
|
+
parent_id=parent_id,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
await self.activity_logger.log(
|
|
188
|
+
resource_type,
|
|
189
|
+
resource_id,
|
|
190
|
+
ActivityAction.COMMENTED,
|
|
191
|
+
actor_id=author_id,
|
|
192
|
+
description="Added a comment",
|
|
193
|
+
metadata={"comment_id": comment.id},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return comment
|
|
197
|
+
|
|
198
|
+
async def update_comment(
|
|
199
|
+
self,
|
|
200
|
+
comment_id: str,
|
|
201
|
+
*,
|
|
202
|
+
content: str,
|
|
203
|
+
actor_id: str | None = None,
|
|
204
|
+
) -> Comment | None:
|
|
205
|
+
"""Update a comment.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
comment_id: Comment ID.
|
|
209
|
+
content: New content.
|
|
210
|
+
actor_id: User updating the comment.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Updated comment or None.
|
|
214
|
+
"""
|
|
215
|
+
comment = await self.comment_repo.get_by_id(comment_id)
|
|
216
|
+
if not comment:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
comment.content = content
|
|
220
|
+
await self.session.flush()
|
|
221
|
+
await self.session.refresh(comment)
|
|
222
|
+
|
|
223
|
+
return comment
|
|
224
|
+
|
|
225
|
+
async def delete_comment(
|
|
226
|
+
self,
|
|
227
|
+
comment_id: str,
|
|
228
|
+
*,
|
|
229
|
+
actor_id: str | None = None,
|
|
230
|
+
) -> bool:
|
|
231
|
+
"""Delete a comment.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
comment_id: Comment ID.
|
|
235
|
+
actor_id: User deleting the comment.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
True if deleted.
|
|
239
|
+
"""
|
|
240
|
+
comment = await self.comment_repo.get_by_id(comment_id)
|
|
241
|
+
if not comment:
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
resource_type = comment.resource_type
|
|
245
|
+
resource_id = comment.resource_id
|
|
246
|
+
|
|
247
|
+
deleted = await self.comment_repo.delete(comment_id)
|
|
248
|
+
|
|
249
|
+
if deleted:
|
|
250
|
+
await self.activity_logger.log(
|
|
251
|
+
resource_type,
|
|
252
|
+
resource_id,
|
|
253
|
+
ActivityAction.DELETED,
|
|
254
|
+
actor_id=actor_id,
|
|
255
|
+
description="Deleted a comment",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return deleted
|
|
259
|
+
|
|
260
|
+
# =========================================================================
|
|
261
|
+
# Activity Operations
|
|
262
|
+
# =========================================================================
|
|
263
|
+
|
|
264
|
+
async def get_activities(
|
|
265
|
+
self,
|
|
266
|
+
*,
|
|
267
|
+
resource_type: str | None = None,
|
|
268
|
+
resource_id: str | None = None,
|
|
269
|
+
limit: int = 50,
|
|
270
|
+
) -> list[Activity]:
|
|
271
|
+
"""Get activities with optional filters.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
resource_type: Filter by resource type.
|
|
275
|
+
resource_id: Filter by resource ID.
|
|
276
|
+
limit: Maximum to return.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
List of activities.
|
|
280
|
+
"""
|
|
281
|
+
if resource_type and resource_id:
|
|
282
|
+
return await self.activity_logger.get_for_resource(
|
|
283
|
+
resource_type,
|
|
284
|
+
resource_id,
|
|
285
|
+
limit=limit,
|
|
286
|
+
)
|
|
287
|
+
return await self.activity_logger.get_recent(
|
|
288
|
+
resource_type=resource_type,
|
|
289
|
+
limit=limit,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def get_recent_activities(
|
|
293
|
+
self,
|
|
294
|
+
*,
|
|
295
|
+
limit: int = 50,
|
|
296
|
+
) -> list[Activity]:
|
|
297
|
+
"""Get recent activities across all resources.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
limit: Maximum to return.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
List of recent activities.
|
|
304
|
+
"""
|
|
305
|
+
return await self.activity_logger.get_recent(limit=limit)
|