Skip to content

Commit 3ebd0f9

Browse files
committed
feat(examples): add code-review SQL schema and ReviewStore
1 parent 0fe2071 commit 3ebd0f9

4 files changed

Lines changed: 307 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
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+
"""Persistence layer for the code-review example."""
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
"""SQLAlchemy declarative models for the code-review example."""
7+
import uuid
8+
from datetime import datetime
9+
10+
from sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Integer, String, Text
11+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
12+
13+
14+
def _uuid() -> str:
15+
return uuid.uuid4().hex
16+
17+
18+
class CrBase(DeclarativeBase):
19+
"""Dedicated base so only this example's tables are created."""
20+
21+
22+
class ReviewTaskRow(CrBase):
23+
__tablename__ = "cr_review_tasks"
24+
25+
id: Mapped[str] = mapped_column(String(64), primary_key=True, default=_uuid)
26+
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
27+
finished_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
28+
status: Mapped[str] = mapped_column(String(32), default="pending")
29+
input_type: Mapped[str] = mapped_column(String(32), default="")
30+
input_ref: Mapped[str] = mapped_column(String(512), default="")
31+
runtime: Mapped[str] = mapped_column(String(32), default="local")
32+
dry_run: Mapped[bool] = mapped_column(Boolean, default=True)
33+
diff_summary: Mapped[dict] = mapped_column(JSON, nullable=True)
34+
35+
36+
class SandboxRunRow(CrBase):
37+
__tablename__ = "cr_sandbox_runs"
38+
39+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
40+
task_id: Mapped[str] = mapped_column(String(64), ForeignKey("cr_review_tasks.id"))
41+
script: Mapped[str] = mapped_column(String(128), default="")
42+
category: Mapped[str] = mapped_column(String(64), default="")
43+
status: Mapped[str] = mapped_column(String(32), default="")
44+
exit_code: Mapped[int] = mapped_column(Integer, default=0)
45+
duration_ms: Mapped[int] = mapped_column(Integer, default=0)
46+
timed_out: Mapped[bool] = mapped_column(Boolean, default=False)
47+
stdout_summary: Mapped[str] = mapped_column(Text, default="")
48+
stderr_summary: Mapped[str] = mapped_column(Text, default="")
49+
error_type: Mapped[str] = mapped_column(String(64), default="")
50+
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
51+
52+
53+
class FindingRow(CrBase):
54+
__tablename__ = "cr_findings"
55+
56+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
57+
task_id: Mapped[str] = mapped_column(String(64), ForeignKey("cr_review_tasks.id"))
58+
severity: Mapped[str] = mapped_column(String(16), default="")
59+
category: Mapped[str] = mapped_column(String(64), default="")
60+
file: Mapped[str] = mapped_column(String(512), default="")
61+
line: Mapped[int] = mapped_column(Integer, default=0)
62+
title: Mapped[str] = mapped_column(String(256), default="")
63+
evidence: Mapped[str] = mapped_column(Text, default="")
64+
recommendation: Mapped[str] = mapped_column(Text, default="")
65+
confidence: Mapped[float] = mapped_column(Float, default=1.0)
66+
source: Mapped[str] = mapped_column(String(32), default="static")
67+
status: Mapped[str] = mapped_column(String(32), default="reported")
68+
dedup_key: Mapped[str] = mapped_column(String(640), default="")
69+
70+
71+
class FilterEventRow(CrBase):
72+
__tablename__ = "cr_filter_events"
73+
74+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
75+
task_id: Mapped[str] = mapped_column(String(64), ForeignKey("cr_review_tasks.id"))
76+
target: Mapped[str] = mapped_column(String(512), default="")
77+
decision: Mapped[str] = mapped_column(String(32), default="")
78+
rule: Mapped[str] = mapped_column(String(64), default="")
79+
reason: Mapped[str] = mapped_column(Text, default="")
80+
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
81+
82+
83+
class MetricsRow(CrBase):
84+
__tablename__ = "cr_metrics"
85+
86+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
87+
task_id: Mapped[str] = mapped_column(String(64), ForeignKey("cr_review_tasks.id"))
88+
total_duration_ms: Mapped[int] = mapped_column(Integer, default=0)
89+
sandbox_duration_ms: Mapped[int] = mapped_column(Integer, default=0)
90+
tool_calls: Mapped[int] = mapped_column(Integer, default=0)
91+
intercepts: Mapped[int] = mapped_column(Integer, default=0)
92+
findings_total: Mapped[int] = mapped_column(Integer, default=0)
93+
severity_distribution: Mapped[dict] = mapped_column(JSON, nullable=True)
94+
error_distribution: Mapped[dict] = mapped_column(JSON, nullable=True)
95+
96+
97+
class ReportRow(CrBase):
98+
__tablename__ = "cr_reports"
99+
100+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
101+
task_id: Mapped[str] = mapped_column(String(64), ForeignKey("cr_review_tasks.id"))
102+
report_json: Mapped[dict] = mapped_column(JSON, nullable=True)
103+
report_md: Mapped[str] = mapped_column(Text, default="")
104+
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
"""Tests for the SQLite-backed ReviewStore."""
7+
from review.findings import Finding
8+
from storage.store import ReviewStore
9+
10+
11+
async def _new_store(tmp_path):
12+
return ReviewStore(db_url=f"sqlite:///{tmp_path}/cr.db")
13+
14+
15+
async def test_task_roundtrip(tmp_path):
16+
store = await _new_store(tmp_path)
17+
task_id = await store.create_task("fixture", "clean.diff", "local", True)
18+
assert task_id
19+
await store.update_task(task_id, status="completed",
20+
diff_summary={"files_changed": 1}, finished=True)
21+
bundle = await store.get_task_bundle(task_id)
22+
assert bundle["task"]["status"] == "completed"
23+
assert bundle["task"]["diff_summary"]["files_changed"] == 1
24+
assert bundle["task"]["dry_run"] is True
25+
await store.close()
26+
27+
28+
async def test_bundle_collects_all_tables(tmp_path):
29+
store = await _new_store(tmp_path)
30+
task_id = await store.create_task("fixture", "x.diff", "local", True)
31+
await store.add_sandbox_run(task_id, script="check_security.py", category="security",
32+
status="ok", exit_code=0, duration_ms=12, timed_out=False,
33+
stdout_summary="{}", stderr_summary="", error_type="")
34+
await store.add_findings(task_id, [Finding(
35+
severity="critical", category="secret_leak", file="a.py", line=3,
36+
title="secret", evidence='key = "sk-abcdefghijklmnopqrstuvwxyz123456"',
37+
confidence=0.95)], status="reported")
38+
await store.add_filter_event(task_id, "rm -rf /", "deny", "risk_command", "destructive command")
39+
await store.add_metrics(task_id, {"total_duration_ms": 1000, "sandbox_duration_ms": 500,
40+
"tool_calls": 6, "intercepts": 1, "findings_total": 1,
41+
"severity_distribution": {"critical": 1},
42+
"error_distribution": {}})
43+
await store.add_report(task_id, {"conclusion": "blocked"}, "# report")
44+
bundle = await store.get_task_bundle(task_id)
45+
assert len(bundle["sandbox_runs"]) == 1
46+
assert len(bundle["findings"]) == 1
47+
assert len(bundle["filter_events"]) == 1
48+
assert bundle["metrics"]["tool_calls"] == 6
49+
assert bundle["report"]["report_json"]["conclusion"] == "blocked"
50+
await store.close()
51+
52+
53+
async def test_store_redacts_evidence_and_output(tmp_path):
54+
store = await _new_store(tmp_path)
55+
task_id = await store.create_task("fixture", "x.diff", "local", True)
56+
secret = "sk-abcdefghijklmnopqrstuvwxyz123456"
57+
await store.add_findings(task_id, [Finding(
58+
severity="critical", category="secret_leak", file="a.py", line=3,
59+
title="secret", evidence=f'key = "{secret}"', confidence=0.95)], status="reported")
60+
await store.add_sandbox_run(task_id, script="s.py", category="c", status="ok",
61+
exit_code=0, duration_ms=1, timed_out=False,
62+
stdout_summary=f"leaked {secret}", stderr_summary="", error_type="")
63+
bundle = await store.get_task_bundle(task_id)
64+
assert secret not in bundle["findings"][0]["evidence"]
65+
assert secret not in bundle["sandbox_runs"][0]["stdout_summary"]
66+
await store.close()
67+
68+
69+
async def test_unknown_task_returns_none_task(tmp_path):
70+
store = await _new_store(tmp_path)
71+
bundle = await store.get_task_bundle("nope")
72+
assert bundle["task"] is None
73+
await store.close()

0 commit comments

Comments
 (0)