CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
"""A thin, unified calling surface over the app's existing Provider layer.
|
|
|
|
The task asks for a ``clients.py`` abstraction that talks to Anthropic / OpenAI
|
|
behind one interface. This app **already has** that — ``providers/`` with
|
|
``build_provider`` and a canonical ``chat()`` that streams text and returns the
|
|
final assistant message. Rather than duplicate it (and re-solve TLS trust,
|
|
429-retry, gateway config…), this module adapts it to the shape the prober
|
|
wants: a single blocking ``complete()`` that returns text + token estimate.
|
|
|
|
Tests inject a fake :class:`ProbeClient` so assessment never hits a real API.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Optional, Protocol
|
|
|
|
|
|
@dataclass
|
|
class CompletionResult:
|
|
"""Outcome of one non-streaming completion used for probing."""
|
|
|
|
text: str = ""
|
|
tokens_out: int = 0
|
|
error: Optional[str] = None
|
|
|
|
@property
|
|
def ok(self) -> bool:
|
|
"""Lượt dò có thành công không (không có lỗi)."""
|
|
return self.error is None
|
|
|
|
|
|
class ProbeClient(Protocol):
|
|
"""Minimal interface the prober/judge depend on (so they're mockable)."""
|
|
|
|
def complete(
|
|
self,
|
|
provider: str,
|
|
model_id: str,
|
|
messages: List[Dict[str, Any]],
|
|
) -> CompletionResult:
|
|
"""Gọi một model và trả về kết quả kèm số token, độ trễ và lỗi (nếu có)."""
|
|
...
|
|
|
|
|
|
def _estimate_tokens(text: str) -> int:
|
|
"""Rough output-token count. Uses the app's estimator when importable
|
|
(keeps the number consistent with the usage tracker), else ~4 chars/token."""
|
|
try:
|
|
from ..usage_tracker import estimate_tokens
|
|
return int(estimate_tokens(text or ""))
|
|
except Exception: # noqa: BLE001
|
|
return max(0, len(text or "") // 4)
|
|
|
|
|
|
class AppProbeClient:
|
|
"""Real :class:`ProbeClient` backed by :class:`AppContext`.
|
|
|
|
Builds a fresh provider per call via ``ctx.build_provider_for`` — the same
|
|
path interactive chat uses — so the internal gateway, per-host TLS trust and
|
|
rate-limit retry all apply to assessment calls too.
|
|
"""
|
|
|
|
def __init__(self, ctx: Any) -> None:
|
|
"""Giữ ``AppContext`` để dựng provider lúc cần thăm dò."""
|
|
self.ctx = ctx
|
|
|
|
def complete(
|
|
self,
|
|
provider: str,
|
|
model_id: str,
|
|
messages: List[Dict[str, Any]],
|
|
) -> CompletionResult:
|
|
"""Gọi model qua provider thật; lỗi được gói vào kết quả chứ không ném ra —
|
|
một model hỏng không được làm dừng cả lượt chấm điểm danh mục.
|
|
"""
|
|
try:
|
|
prov = self.ctx.build_provider_for(provider, model_id or None)
|
|
# Non-streaming: no on_text/on_reasoning callbacks. cancel=None.
|
|
result = prov.chat(messages, tools=None, on_text=None, cancel=None)
|
|
except Exception as exc: # noqa: BLE001 — surfaced as a failed probe
|
|
return CompletionResult(error=str(exc))
|
|
content = ""
|
|
if isinstance(result, dict):
|
|
content = result.get("content") or ""
|
|
# Strip any inline <think> block a reasoning model may have inlined.
|
|
try:
|
|
from ...providers.base import Provider
|
|
content = Provider.strip_think(content)
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
return CompletionResult(text=content, tokens_out=_estimate_tokens(content))
|
|
|
|
|
|
__all__ = ["CompletionResult", "ProbeClient", "AppProbeClient"]
|