Skip to content

Commit 821f703

Browse files
committed
examples: add fake-model agent loop to skills_code_review_agent
Wire the review through the framework: an LlmAgent with a review_code FunctionTool, driven by a FakeReviewModel that needs no API key. On the first turn the fake model emits a single tool call with the user's diff; on the second turn (after the deterministic pipeline runs behind the tool) it summarizes the findings. run_agent.py demos it; a smoke test asserts the tool is called, a summary is produced, and no secret leaks. The guard Filter will attach on the tool via filters_name (TOOL scope), not on the agent, in a follow-up slice. Updates #92 RELEASE NOTES: NONE
1 parent 802ee49 commit 821f703

8 files changed

Lines changed: 282 additions & 5 deletions

File tree

examples/skills_code_review_agent/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ python run_review.py --repo-path /path/to/repo --no-db
2020

2121
# Scored self-test over the labelled fixtures (detection-rate / false-positive-rate):
2222
python selftest.py
23+
24+
# Run the review through the LlmAgent (fake model, no API key needed):
25+
python run_agent.py --fixture 0001_insecure.diff
2326
```
2427

2528
## How it works
@@ -74,7 +77,7 @@ a rendered report — criterion 5 is binary-checked, so redaction is centralized
7477

7578
## Status
7679

77-
Implemented: deterministic pipeline, DB persistence, 8 fixtures, scored self-test, CLI. Baseline
78-
secret redaction is in place. Planned follow-up slices: in-sandbox execution (Container/Cube), the
79-
tool-level Filter gate, redaction hardening to ≥95%, OpenTelemetry metrics wiring, and the
80-
fake-model agent loop that drives the review as a Skill tool.
80+
Implemented: deterministic pipeline, DB persistence, 8 fixtures, scored self-test, CLI, and the
81+
fake-model agent loop (`run_agent.py` drives the review through `LlmAgent` + a `FunctionTool` with no
82+
API key). Baseline secret redaction is in place. Planned follow-up slices: in-sandbox execution
83+
(Container/Cube), the tool-level Filter gate, redaction hardening to ≥95%, and OpenTelemetry metrics.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
7+
"""Construct the code-review LlmAgent (Skills + tool), used by run_agent.py."""
8+
from __future__ import annotations
9+
10+
from trpc_agent_sdk.agents import LlmAgent
11+
12+
from .config import get_model
13+
from .prompts import INSTRUCTION
14+
from .tools import build_review_tool
15+
16+
17+
def create_agent() -> LlmAgent:
18+
"""An LlmAgent that reviews a diff by calling the review_code tool, then summarizes.
19+
20+
The guard Filter (slice 3) attaches on the tool via ``filters_name`` — a TOOL-scoped filter,
21+
not on the agent (which resolves in the AGENT namespace and would raise).
22+
"""
23+
return LlmAgent(
24+
name="code_review_agent",
25+
model=get_model(),
26+
instruction=INSTRUCTION,
27+
tools=[build_review_tool()],
28+
)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
7+
"""Configuration for the agent path — model selection and defaults."""
8+
from __future__ import annotations
9+
10+
import os
11+
12+
from trpc_agent_sdk.models import LLMModel
13+
14+
from .model import FakeReviewModel
15+
16+
17+
def get_model() -> LLMModel:
18+
"""Return the fake model by default (no API key); a real OpenAI model if one is configured."""
19+
api_key = os.getenv("TRPC_AGENT_API_KEY")
20+
if api_key:
21+
from trpc_agent_sdk.models import OpenAIModel
22+
23+
return OpenAIModel(
24+
model_name=os.getenv("MODEL_NAME", "gpt-4o-mini"),
25+
api_key=api_key,
26+
base_url=os.getenv("TRPC_AGENT_BASE_URL"),
27+
)
28+
return FakeReviewModel(model_name="fake-review-1")
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+
7+
"""FakeReviewModel — deterministic, no-API-key model for the dry-run agent path (criterion 6/8).
8+
9+
It does not call any LLM. On the first turn it emits a single tool call to ``review_code`` with the
10+
user's diff; on the second turn (after the tool result comes back) it emits a short text summary.
11+
This lets the Skills + tool + telemetry agent path run in CI with no secrets, while the actual
12+
findings come from the deterministic scanner pipeline behind the tool.
13+
"""
14+
from __future__ import annotations
15+
16+
from typing import AsyncGenerator, List, Optional
17+
18+
from trpc_agent_sdk.models import LlmRequest, LlmResponse, LLMModel
19+
from trpc_agent_sdk.types import Content, FunctionCall, Part
20+
21+
_TOOL_NAME = "review_code"
22+
23+
24+
def _iter_parts(request: LlmRequest):
25+
for content in request.contents or []:
26+
for part in content.parts or []:
27+
yield content, part
28+
29+
30+
def _has_tool_result(request: LlmRequest) -> Optional[dict]:
31+
"""Return the tool's response dict if this request already carries one (the second turn)."""
32+
for _content, part in _iter_parts(request):
33+
if part is not None and part.function_response is not None:
34+
return part.function_response.response or {}
35+
return None
36+
37+
38+
def _last_user_diff(request: LlmRequest) -> str:
39+
"""The diff to review is the most recent user text part."""
40+
latest = ""
41+
for content, part in _iter_parts(request):
42+
if part is not None and part.text and (content.role or "user") == "user":
43+
latest = part.text
44+
return latest
45+
46+
47+
class FakeReviewModel(LLMModel):
48+
49+
@classmethod
50+
def supported_models(cls) -> List[str]:
51+
return [r"fake-review.*"]
52+
53+
def validate_request(self, request: LlmRequest) -> None: # no external validation needed
54+
return None
55+
56+
async def _generate_async_impl(self,
57+
request: LlmRequest,
58+
stream: bool = False,
59+
ctx: object = None) -> AsyncGenerator[LlmResponse, None]:
60+
tool_result = _has_tool_result(request)
61+
if tool_result is not None:
62+
summary = tool_result.get("summary", {})
63+
sev = tool_result.get("severity", {})
64+
text = (f"Review complete (task {tool_result.get('task_id', '?')}). "
65+
f"{summary.get('total', 0)} active finding(s) "
66+
f"[critical={sev.get('critical', 0)}, high={sev.get('high', 0)}, "
67+
f"medium={sev.get('medium', 0)}, low={sev.get('low', 0)}], "
68+
f"{summary.get('needs_human_review', 0)} for human review. "
69+
f"See review_report.json for details.")
70+
yield LlmResponse(content=Content(role="model", parts=[Part(text=text)]))
71+
return
72+
73+
diff = _last_user_diff(request)
74+
call = FunctionCall(id="cr-call-1", name=_TOOL_NAME, args={"diff_text": diff})
75+
yield LlmResponse(content=Content(role="model", parts=[Part(function_call=call)]))
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
7+
"""System instruction for the code-review agent's LLM finding source."""
8+
9+
INSTRUCTION = ("You are an automated code reviewer. When given a diff, call the `review_code` tool with the "
10+
"diff text to run the static-analysis pipeline, then summarize the findings for the user: how "
11+
"many issues by severity, and the most important ones to fix first. Be concise and specific.")
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
7+
"""The review tool the agent calls — a thin wrapper over the deterministic pipeline."""
8+
from __future__ import annotations
9+
10+
from typing import Any
11+
12+
from trpc_agent_sdk.tools import FunctionTool
13+
14+
from pipeline.engine import run_review
15+
16+
17+
def review_code(diff_text: str) -> dict[str, Any]:
18+
"""Run the code-review pipeline on a unified diff and return a findings summary.
19+
20+
Args:
21+
diff_text: the unified diff to review.
22+
"""
23+
result = run_review(diff_text=diff_text)
24+
return {
25+
"task_id": result.task_id,
26+
"summary": result.report.findings_summary,
27+
"severity": result.report.severity_stats,
28+
}
29+
30+
31+
def build_review_tool() -> FunctionTool:
32+
return FunctionTool(review_code)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env python3
2+
3+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
4+
#
5+
# Copyright (C) 2026 Tencent. All rights reserved.
6+
#
7+
# tRPC-Agent-Python is licensed under Apache-2.0.
8+
9+
"""Run a code review through the LlmAgent (Skills + tool) — the framework-exercising path.
10+
11+
Dry-run by default: with no API key, FakeReviewModel drives one call to the review_code tool and
12+
summarizes the result — no LLM, no secrets. Set TRPC_AGENT_API_KEY to use a real model instead.
13+
14+
python run_agent.py --fixture 0001_insecure.diff
15+
"""
16+
from __future__ import annotations
17+
18+
import argparse
19+
import asyncio
20+
import uuid
21+
from pathlib import Path
22+
23+
from dotenv import load_dotenv
24+
25+
from trpc_agent_sdk.runners import Runner
26+
from trpc_agent_sdk.sessions import InMemorySessionService
27+
from trpc_agent_sdk.types import Content, Part
28+
29+
from agent.agent import create_agent
30+
31+
HERE = Path(__file__).parent
32+
33+
34+
async def review(diff_text: str) -> None:
35+
app_name = "code_review_agent"
36+
agent = create_agent()
37+
runner = Runner(app_name=app_name, agent=agent, session_service=InMemorySessionService())
38+
39+
user_id, session_id = "reviewer", str(uuid.uuid4())
40+
await runner.session_service.create_session(app_name=app_name, user_id=user_id, session_id=session_id)
41+
42+
message = Content(role="user", parts=[Part(text=diff_text)])
43+
async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=message):
44+
if not event.content or not event.content.parts:
45+
continue
46+
for part in event.content.parts:
47+
if part.text:
48+
print(part.text, end="", flush=True)
49+
elif part.function_call:
50+
print(f"\n[tool call] {part.function_call.name}")
51+
elif part.function_response:
52+
print(f"[tool result] {part.function_response.response}")
53+
print()
54+
55+
56+
def main() -> None:
57+
load_dotenv()
58+
ap = argparse.ArgumentParser(description="Review a diff via the code-review agent.")
59+
src = ap.add_mutually_exclusive_group(required=True)
60+
src.add_argument("--diff-file")
61+
src.add_argument("--fixture")
62+
args = ap.parse_args()
63+
path = Path(args.diff_file) if args.diff_file else HERE / "fixtures" / "diffs" / args.fixture
64+
asyncio.run(review(path.read_text(encoding="utf-8")))
65+
66+
67+
if __name__ == "__main__":
68+
main()

tests/examples/test_skills_code_review_agent.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# Copyright (C) 2026 Tencent. All rights reserved.
44
#
55
# tRPC-Agent-Python is licensed under Apache-2.0.
6-
76
"""Smoke tests for the skills_code_review_agent example.
87
98
Deterministic and fast: no real model, no Docker. Verifies the dry-run pipeline detects issues,
@@ -106,3 +105,36 @@ async def test_persist_and_query_no_secret_leak(tmp_path) -> None:
106105
raw = db_file.read_bytes()
107106
for secret in _SECRETS:
108107
assert secret.encode() not in raw
108+
109+
110+
@pytest.mark.asyncio
111+
async def test_agent_path_calls_tool_and_summarizes() -> None:
112+
"""The fake-model agent loop drives the review_code tool and summarizes — no API key."""
113+
import uuid
114+
115+
from trpc_agent_sdk.runners import Runner
116+
from trpc_agent_sdk.sessions import InMemorySessionService
117+
from trpc_agent_sdk.types import Content, Part
118+
119+
from agent.agent import create_agent
120+
121+
runner = Runner(app_name="cr_test", agent=create_agent(), session_service=InMemorySessionService())
122+
sid = str(uuid.uuid4())
123+
await runner.session_service.create_session(app_name="cr_test", user_id="u", session_id=sid)
124+
125+
diff = (_FIXTURES / "0001_insecure.diff").read_text()
126+
saw_tool_call = False
127+
final_text = ""
128+
async for event in runner.run_async(user_id="u",
129+
session_id=sid,
130+
new_message=Content(role="user", parts=[Part(text=diff)])):
131+
for part in (event.content.parts if event.content else []) or []:
132+
if part.function_call:
133+
saw_tool_call = True
134+
if part.text:
135+
final_text += part.text
136+
137+
assert saw_tool_call
138+
assert "Review complete" in final_text
139+
for secret in _SECRETS:
140+
assert secret not in final_text

0 commit comments

Comments
 (0)