From 4b00eed5660131e83675242a78927f1cce530892 Mon Sep 17 00:00:00 2001 From: Sirius DevOps Date: Thu, 27 Aug 2026 22:30:50 -0400 Subject: [PATCH] feat: nous portal chat client for news summarizer --- news/summerizer/nous_client.py | 36 +++++++++ news/summerizer/tests/test_nous_client.py | 97 +++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 news/summerizer/nous_client.py create mode 100644 news/summerizer/tests/test_nous_client.py diff --git a/news/summerizer/nous_client.py b/news/summerizer/nous_client.py new file mode 100644 index 0000000..da0b2c4 --- /dev/null +++ b/news/summerizer/nous_client.py @@ -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 "" diff --git a/news/summerizer/tests/test_nous_client.py b/news/summerizer/tests/test_nous_client.py new file mode 100644 index 0000000..002faa4 --- /dev/null +++ b/news/summerizer/tests/test_nous_client.py @@ -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) == ""