|
| 1 | +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. |
| 2 | +# |
| 3 | +# Copyright (C) 2026 Tencent. All rights reserved. |
| 4 | +# |
| 5 | +# tRPC-Agent-Python is licensed under Apache-2.0. |
| 6 | +"""ReviewStore: persistence built on the SDK's SqlStorage (SQLite by default, |
| 7 | +any SQLAlchemy db_url works).""" |
| 8 | +from datetime import datetime |
| 9 | + |
| 10 | +from trpc_agent_sdk.storage import SqlCondition, SqlKey, SqlStorage |
| 11 | + |
| 12 | +from review.redaction import redact_text |
| 13 | + |
| 14 | +from .models import (CrBase, FilterEventRow, FindingRow, MetricsRow, ReportRow, |
| 15 | + ReviewTaskRow, SandboxRunRow) |
| 16 | + |
| 17 | + |
| 18 | +def _row_to_dict(row): |
| 19 | + if row is None: |
| 20 | + return None |
| 21 | + return {c.name: getattr(row, c.name) for c in row.__table__.columns} |
| 22 | + |
| 23 | + |
| 24 | +class ReviewStore: |
| 25 | + """CRUD facade over the code-review tables. All write paths redact secrets.""" |
| 26 | + |
| 27 | + def __init__(self, db_url: str = "sqlite:///code_review.db"): |
| 28 | + self._storage = SqlStorage(is_async=False, db_url=db_url, metadata=CrBase.metadata) |
| 29 | + |
| 30 | + async def _add(self, row) -> None: |
| 31 | + async with self._storage.create_db_session() as db: |
| 32 | + await self._storage.add(db, row) |
| 33 | + await self._storage.commit(db) |
| 34 | + |
| 35 | + async def create_task(self, input_type: str, input_ref: str, runtime: str, |
| 36 | + dry_run: bool) -> str: |
| 37 | + row = ReviewTaskRow(input_type=input_type, input_ref=input_ref, |
| 38 | + runtime=runtime, dry_run=dry_run, status="running") |
| 39 | + task_id = row.id or "" |
| 40 | + if not task_id: |
| 41 | + import uuid |
| 42 | + task_id = uuid.uuid4().hex |
| 43 | + row.id = task_id |
| 44 | + await self._add(row) |
| 45 | + return task_id |
| 46 | + |
| 47 | + async def update_task(self, task_id: str, status: str = None, |
| 48 | + diff_summary: dict = None, finished: bool = False) -> None: |
| 49 | + async with self._storage.create_db_session() as db: |
| 50 | + row = await self._storage.get(db, SqlKey(key=(task_id,), storage_cls=ReviewTaskRow)) |
| 51 | + if row is None: |
| 52 | + return |
| 53 | + if status is not None: |
| 54 | + row.status = status |
| 55 | + if diff_summary is not None: |
| 56 | + row.diff_summary = diff_summary |
| 57 | + if finished: |
| 58 | + row.finished_at = datetime.now() |
| 59 | + await self._storage.commit(db) |
| 60 | + |
| 61 | + async def add_sandbox_run(self, task_id: str, *, script: str, category: str, |
| 62 | + status: str, exit_code: int, duration_ms: int, |
| 63 | + timed_out: bool, stdout_summary: str, |
| 64 | + stderr_summary: str, error_type: str) -> None: |
| 65 | + await self._add(SandboxRunRow( |
| 66 | + task_id=task_id, script=script, category=category, status=status, |
| 67 | + exit_code=exit_code, duration_ms=duration_ms, timed_out=timed_out, |
| 68 | + stdout_summary=redact_text(stdout_summary), |
| 69 | + stderr_summary=redact_text(stderr_summary), error_type=error_type)) |
| 70 | + |
| 71 | + async def add_findings(self, task_id: str, findings, status: str) -> None: |
| 72 | + for f in findings: |
| 73 | + await self._add(FindingRow( |
| 74 | + task_id=task_id, severity=f.severity, category=f.category, |
| 75 | + file=f.file, line=f.line, title=f.title, |
| 76 | + evidence=redact_text(f.evidence), recommendation=f.recommendation, |
| 77 | + confidence=f.confidence, source=f.source, status=status, |
| 78 | + dedup_key=f.dedup_key)) |
| 79 | + |
| 80 | + async def add_filter_event(self, task_id: str, target: str, decision: str, |
| 81 | + rule: str, reason: str) -> None: |
| 82 | + await self._add(FilterEventRow(task_id=task_id, target=redact_text(target), |
| 83 | + decision=decision, rule=rule, reason=reason)) |
| 84 | + |
| 85 | + async def add_metrics(self, task_id: str, metrics: dict) -> None: |
| 86 | + await self._add(MetricsRow( |
| 87 | + task_id=task_id, |
| 88 | + total_duration_ms=metrics.get("total_duration_ms", 0), |
| 89 | + sandbox_duration_ms=metrics.get("sandbox_duration_ms", 0), |
| 90 | + tool_calls=metrics.get("tool_calls", 0), |
| 91 | + intercepts=metrics.get("intercepts", 0), |
| 92 | + findings_total=metrics.get("findings_total", 0), |
| 93 | + severity_distribution=metrics.get("severity_distribution", {}), |
| 94 | + error_distribution=metrics.get("error_distribution", {}))) |
| 95 | + |
| 96 | + async def add_report(self, task_id: str, report_json: dict, report_md: str) -> None: |
| 97 | + await self._add(ReportRow(task_id=task_id, report_json=report_json, |
| 98 | + report_md=redact_text(report_md))) |
| 99 | + |
| 100 | + async def _query_all(self, db, storage_cls, task_id): |
| 101 | + rows = await self._storage.query( |
| 102 | + db, SqlKey(key=(), storage_cls=storage_cls), |
| 103 | + SqlCondition(filters=[storage_cls.task_id == task_id])) |
| 104 | + return [_row_to_dict(r) for r in rows] |
| 105 | + |
| 106 | + async def get_task_bundle(self, task_id: str) -> dict: |
| 107 | + async with self._storage.create_db_session() as db: |
| 108 | + task = await self._storage.get(db, SqlKey(key=(task_id,), storage_cls=ReviewTaskRow)) |
| 109 | + runs = await self._query_all(db, SandboxRunRow, task_id) |
| 110 | + findings = await self._query_all(db, FindingRow, task_id) |
| 111 | + events = await self._query_all(db, FilterEventRow, task_id) |
| 112 | + metrics_rows = await self._query_all(db, MetricsRow, task_id) |
| 113 | + report_rows = await self._query_all(db, ReportRow, task_id) |
| 114 | + return { |
| 115 | + "task": _row_to_dict(task), |
| 116 | + "sandbox_runs": runs, |
| 117 | + "findings": findings, |
| 118 | + "filter_events": events, |
| 119 | + "metrics": metrics_rows[0] if metrics_rows else None, |
| 120 | + "report": report_rows[0] if report_rows else None, |
| 121 | + } |
| 122 | + |
| 123 | + async def close(self) -> None: |
| 124 | + await self._storage.close() |
0 commit comments