|
| 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)])) |
0 commit comments