feat: nous portal chat client for news summarizer
This commit is contained in:
parent
96590bc16d
commit
4b00eed566
2 changed files with 133 additions and 0 deletions
36
news/summerizer/nous_client.py
Normal file
36
news/summerizer/nous_client.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"""HTTP client for the Nous inference chat completions API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
_DEFAULT_UA = "osint-dashboard-news-summarizer"
|
||||
_DEFAULT_BASE = "https://inference-api.nousresearch.com/v1"
|
||||
|
||||
|
||||
def chat(prompt, *, api_key, model, base_url, json_mode=False) -> str:
|
||||
resolved = (base_url or os.environ.get("NOUS_BASE_URL", _DEFAULT_BASE)).rstrip("/")
|
||||
url = f"{resolved}/chat/completions"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"User-Agent": os.environ.get("OSINT_USER_AGENT") or _DEFAULT_UA,
|
||||
}
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 4096,
|
||||
}
|
||||
if json_mode:
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
try:
|
||||
with httpx.Client(timeout=60.0) as client:
|
||||
resp = client.post(url, headers=headers, json=payload)
|
||||
if resp.status_code == 401 or resp.status_code >= 500:
|
||||
return ""
|
||||
data = resp.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
except Exception:
|
||||
return ""
|
||||
97
news/summerizer/tests/test_nous_client.py
Normal file
97
news/summerizer/tests/test_nous_client.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
from nous_client import chat
|
||||
|
||||
DEFAULT_UA = "osint-dashboard-news-summarizer"
|
||||
BASE = "https://inference-api.nousresearch.com/v1"
|
||||
|
||||
|
||||
def _ok_response(content="hello"):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 200
|
||||
resp.json.return_value = {"choices": [{"message": {"content": content}}]}
|
||||
return resp
|
||||
|
||||
|
||||
def _install_fake(monkeypatch, post_impl):
|
||||
captured = {}
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, timeout=None, **kwargs):
|
||||
captured["timeout"] = timeout
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
def post(self, url, *, headers=None, json=None, **kwargs):
|
||||
captured["url"] = url
|
||||
captured["headers"] = headers
|
||||
captured["json"] = json
|
||||
return post_impl(url, headers, json)
|
||||
|
||||
monkeypatch.setattr(httpx, "Client", FakeClient)
|
||||
return captured
|
||||
|
||||
|
||||
def test_posts_chat_completions_with_auth_body_and_returns_content(monkeypatch):
|
||||
captured = _install_fake(monkeypatch, lambda *a: _ok_response("the-content"))
|
||||
out = chat(
|
||||
"summarize this",
|
||||
api_key="secret-key",
|
||||
model="hermes-3",
|
||||
base_url=BASE,
|
||||
)
|
||||
assert out == "the-content"
|
||||
assert captured["url"] == f"{BASE}/chat/completions"
|
||||
assert captured["headers"]["Authorization"] == "Bearer secret-key"
|
||||
assert captured["headers"]["User-Agent"] == DEFAULT_UA
|
||||
assert captured["json"]["model"] == "hermes-3"
|
||||
assert captured["json"]["messages"] == [{"role": "user", "content": "summarize this"}]
|
||||
assert captured["json"]["temperature"] == 0.2
|
||||
assert captured["json"]["max_tokens"] == 4096
|
||||
assert "response_format" not in captured["json"]
|
||||
|
||||
|
||||
def test_user_agent_equals_osint_user_agent_env(monkeypatch):
|
||||
monkeypatch.setenv("OSINT_USER_AGENT", "custom-ua/2.0")
|
||||
captured = _install_fake(monkeypatch, lambda *a: _ok_response("ok"))
|
||||
chat("p", api_key="k", model="m", base_url=BASE)
|
||||
assert captured["headers"]["User-Agent"] == "custom-ua/2.0"
|
||||
|
||||
|
||||
def test_json_mode_sets_response_format(monkeypatch):
|
||||
captured = _install_fake(monkeypatch, lambda *a: _ok_response("{}"))
|
||||
chat("p", api_key="k", model="m", base_url=BASE, json_mode=True)
|
||||
assert captured["json"]["response_format"] == {"type": "json_object"}
|
||||
|
||||
|
||||
def test_401_returns_empty_string(monkeypatch):
|
||||
def post_impl(*a):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 401
|
||||
return resp
|
||||
|
||||
_install_fake(monkeypatch, post_impl)
|
||||
assert chat("p", api_key="bad", model="m", base_url=BASE) == ""
|
||||
|
||||
|
||||
def test_5xx_returns_empty_string(monkeypatch):
|
||||
def post_impl(*a):
|
||||
resp = MagicMock()
|
||||
resp.status_code = 503
|
||||
return resp
|
||||
|
||||
_install_fake(monkeypatch, post_impl)
|
||||
assert chat("p", api_key="k", model="m", base_url=BASE) == ""
|
||||
|
||||
|
||||
def test_timeout_returns_empty_string(monkeypatch):
|
||||
def post_impl(*a):
|
||||
raise httpx.TimeoutException("timed out")
|
||||
|
||||
_install_fake(monkeypatch, post_impl)
|
||||
assert chat("p", api_key="k", model="m", base_url=BASE) == ""
|
||||
Loading…
Add table
Reference in a new issue