Skip to content

Commit e4c02fd

Browse files
committed
feat(examples): add fake review model, agent wiring and llm review step
1 parent ec6a74e commit e4c02fd

7 files changed

Lines changed: 250 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+
"""Agent wiring — fake model, prompts, config, and LlmAgent factory."""
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
"""LlmAgent wiring for the code review example."""
7+
from trpc_agent_sdk.agents import LlmAgent
8+
from trpc_agent_sdk.models import OpenAIModel
9+
from trpc_agent_sdk.skills import SkillToolSet
10+
11+
from .config import get_model_config
12+
from .fake_model import FakeReviewModel
13+
from .prompts import INSTRUCTION
14+
15+
16+
def create_review_agent(repository, dry_run: bool, tool_filters: list) -> LlmAgent:
17+
"""Create the review agent. tool_filters guard skill_run via governance."""
18+
if dry_run:
19+
model = FakeReviewModel()
20+
else:
21+
api_key, url, model_name = get_model_config()
22+
model = OpenAIModel(model_name=model_name, api_key=api_key, base_url=url)
23+
toolset = SkillToolSet(repository=repository, filters=list(tool_filters))
24+
return LlmAgent(
25+
name="code_review_agent",
26+
description="Automated code review agent combining skill scripts and LLM analysis.",
27+
model=model,
28+
instruction=INSTRUCTION,
29+
tools=[toolset],
30+
skill_repository=repository,
31+
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
"""Model configuration from environment variables."""
7+
import os
8+
9+
10+
def get_model_config() -> tuple[str, str, str]:
11+
"""Return (api_key, base_url, model_name) or raise when unset."""
12+
api_key = os.getenv("TRPC_AGENT_API_KEY", "")
13+
url = os.getenv("TRPC_AGENT_BASE_URL", "")
14+
model_name = os.getenv("TRPC_AGENT_MODEL_NAME", "")
15+
if not api_key or not url or not model_name:
16+
raise ValueError("TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL and "
17+
"TRPC_AGENT_MODEL_NAME must be set for real-model mode")
18+
return api_key, url, model_name
19+
20+
21+
def is_dry_run(explicit: bool) -> bool:
22+
"""Dry-run when requested explicitly or when no API key is configured."""
23+
return explicit or not os.getenv("TRPC_AGENT_API_KEY", "")
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
"""Deterministic fake model so the full pipeline runs without any API key."""
7+
import json
8+
from typing import List
9+
10+
from trpc_agent_sdk.models import LLMModel, LlmResponse
11+
from trpc_agent_sdk.types import Content, Part
12+
13+
_FAKE_PAYLOAD = {
14+
"summary": "Dry-run review complete. Static findings are authoritative.",
15+
"findings": [],
16+
}
17+
18+
19+
class FakeReviewModel(LLMModel):
20+
"""LLMModel returning one canned JSON review response."""
21+
22+
def __init__(self, model_name: str = "fake-review-model", **kwargs):
23+
super().__init__(model_name=model_name, **kwargs)
24+
25+
@classmethod
26+
def supported_models(cls) -> List[str]:
27+
return [r"fake-review-.*"]
28+
29+
def validate_request(self, request) -> None:
30+
return None
31+
32+
async def _generate_async_impl(self, request, stream=False, ctx=None):
33+
text = json.dumps(_FAKE_PAYLOAD)
34+
yield LlmResponse(content=Content(role="model", parts=[Part.from_text(text=text)]))
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
"""Prompts for the code review agent."""
7+
8+
INSTRUCTION = """You are an automated code reviewer.
9+
10+
You receive a unified diff plus baseline findings produced by static rule
11+
scripts from the code-review skill. Confirm the baseline findings and add any
12+
extra issues you can justify from the diff alone.
13+
14+
Reply with a single JSON object and nothing else:
15+
{"summary": "<one paragraph>", "findings": [{"severity": "critical|high|medium|low|info",
16+
"category": "security|async_resource_leak|db_lifecycle|missing_test|secret_leak",
17+
"file": "<path>", "line": <int>, "title": "<short>", "evidence": "<code excerpt>",
18+
"recommendation": "<fix>", "confidence": <0.0-1.0>}]}
19+
20+
Only report issues visible in the diff. Do not repeat baseline findings.
21+
"""
22+
23+
REVIEW_REQUEST_TEMPLATE = """Review this change.
24+
25+
Baseline static findings (JSON):
26+
{findings_json}
27+
28+
Unified diff (secrets already redacted):
29+
{diff}
30+
"""
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
"""LLM enrichment step: run the agent once over the diff + static findings."""
7+
import json
8+
import re
9+
import uuid
10+
11+
from trpc_agent_sdk.runners import Runner
12+
from trpc_agent_sdk.sessions import InMemorySessionService
13+
from trpc_agent_sdk.types import Content, Part
14+
15+
from agent.prompts import REVIEW_REQUEST_TEMPLATE
16+
17+
from .findings import Finding
18+
from .redaction import redact_text
19+
20+
_FENCE_RE = re.compile(r"```(?:json)?\s*(\{.*?\})\s*```", re.DOTALL)
21+
22+
23+
def parse_llm_output(text: str):
24+
"""Extract (findings_dicts, summary) from model text; ([], "") on failure."""
25+
candidates = [text.strip()]
26+
fence = _FENCE_RE.search(text)
27+
if fence:
28+
candidates.insert(0, fence.group(1))
29+
for candidate in candidates:
30+
try:
31+
payload = json.loads(candidate)
32+
except (json.JSONDecodeError, ValueError):
33+
continue
34+
if isinstance(payload, dict):
35+
return list(payload.get("findings") or []), str(payload.get("summary") or "")
36+
return [], ""
37+
38+
39+
async def run_llm_review(agent, diff_text: str, static_findings):
40+
"""Run one review turn. Returns (llm_findings, summary, warnings)."""
41+
warnings = []
42+
runner = Runner(app_name="code_review_agent", agent=agent,
43+
session_service=InMemorySessionService())
44+
prompt = REVIEW_REQUEST_TEMPLATE.format(
45+
findings_json=json.dumps([f.model_dump() for f in static_findings]),
46+
diff=redact_text(diff_text))
47+
message = Content(role="user", parts=[Part.from_text(text=prompt)])
48+
final_text = ""
49+
async for event in runner.run_async(user_id="cr_user",
50+
session_id=uuid.uuid4().hex,
51+
new_message=message):
52+
if event.partial or not event.content or not event.content.parts:
53+
continue
54+
for part in event.content.parts:
55+
if getattr(part, "text", None) and not getattr(part, "thought", None):
56+
final_text += part.text
57+
raw_findings, summary = parse_llm_output(final_text)
58+
if not summary and final_text:
59+
warnings.append("llm output was not valid JSON; ignored")
60+
findings = []
61+
for raw in raw_findings:
62+
try:
63+
findings.append(Finding(
64+
severity=str(raw.get("severity", "info")),
65+
category=str(raw.get("category", "security")),
66+
file=str(raw.get("file", "")),
67+
line=int(raw.get("line", 0)),
68+
title=str(raw.get("title", "")),
69+
evidence=str(raw.get("evidence", "")),
70+
recommendation=str(raw.get("recommendation", "")),
71+
confidence=float(raw.get("confidence", 0.5)),
72+
source="llm"))
73+
except (TypeError, ValueError):
74+
warnings.append(f"skipped malformed llm finding: {raw!r}")
75+
return findings, summary, warnings
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 fake model and the LLM review step."""
7+
from pathlib import Path
8+
9+
from trpc_agent_sdk.skills import create_default_skill_repository
10+
11+
from agent.agent import create_review_agent
12+
from agent.fake_model import FakeReviewModel
13+
from review.findings import Finding
14+
from review.llm_review import parse_llm_output, run_llm_review
15+
16+
EXAMPLE_ROOT = Path(__file__).resolve().parents[1]
17+
SKILLS_DIR = EXAMPLE_ROOT / "skills"
18+
19+
20+
def test_parse_llm_output_plain_json():
21+
findings, summary = parse_llm_output(
22+
'{"summary": "ok", "findings": [{"severity": "high", "category": "security",'
23+
' "file": "a.py", "line": 3, "title": "x", "confidence": 0.8}]}')
24+
assert summary == "ok"
25+
assert findings[0]["line"] == 3
26+
27+
28+
def test_parse_llm_output_fenced_json():
29+
text = 'Here you go:\n```json\n{"summary": "s", "findings": []}\n```\n'
30+
findings, summary = parse_llm_output(text)
31+
assert summary == "s" and findings == []
32+
33+
34+
def test_parse_llm_output_garbage_returns_empty():
35+
findings, summary = parse_llm_output("I could not review this.")
36+
assert findings == [] and summary == ""
37+
38+
39+
def test_fake_model_supported_models():
40+
assert FakeReviewModel.supported_models() == [r"fake-review-.*"]
41+
42+
43+
async def test_run_llm_review_with_fake_model():
44+
repository = create_default_skill_repository(str(SKILLS_DIR))
45+
agent = create_review_agent(repository, dry_run=True, tool_filters=[])
46+
static = [Finding(severity="high", category="security", file="a.py", line=1,
47+
title="eval", confidence=0.9)]
48+
llm_findings, summary, warnings = await run_llm_review(agent, "diff text", static)
49+
assert llm_findings == []
50+
assert "Dry-run review complete" in summary
51+
assert warnings == []

0 commit comments

Comments
 (0)