Files
cowork-local/core/routing/clients.py
T
f9f6bc01fd
CI / test (push) Canceled after 0s
Feature/delta team/epic r04 (#7)
## 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>
2026-08-31 05:15:13 +00:00

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"]