fix: auto-fix ruff linting errors and remove unsupported upload-artifact
This commit is contained in:
@@ -57,12 +57,6 @@ jobs:
|
||||
- name: Run tests with coverage
|
||||
run: uv run pytest --cov=handler_base --cov-report=xml --cov-report=term
|
||||
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage.xml
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -8,11 +8,12 @@ Provides consistent patterns for:
|
||||
- Graceful shutdown
|
||||
- Service client wrappers
|
||||
"""
|
||||
|
||||
from handler_base.config import Settings
|
||||
from handler_base.handler import Handler
|
||||
from handler_base.health import HealthServer
|
||||
from handler_base.nats_client import NATSClient
|
||||
from handler_base.telemetry import setup_telemetry, get_tracer, get_meter
|
||||
from handler_base.telemetry import get_meter, get_tracer, setup_telemetry
|
||||
|
||||
__all__ = [
|
||||
"Handler",
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""
|
||||
Service client wrappers for AI/ML backends.
|
||||
"""
|
||||
|
||||
from handler_base.clients.embeddings import EmbeddingsClient
|
||||
from handler_base.clients.reranker import RerankerClient
|
||||
from handler_base.clients.llm import LLMClient
|
||||
from handler_base.clients.tts import TTSClient
|
||||
from handler_base.clients.stt import STTClient
|
||||
from handler_base.clients.milvus import MilvusClient
|
||||
from handler_base.clients.reranker import RerankerClient
|
||||
from handler_base.clients.stt import STTClient
|
||||
from handler_base.clients.tts import TTSClient
|
||||
|
||||
__all__ = [
|
||||
"EmbeddingsClient",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Embeddings service client (Infinity/BGE).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""
|
||||
LLM service client (vLLM/OpenAI-compatible).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, AsyncIterator
|
||||
from typing import AsyncIterator, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -131,9 +132,7 @@ class LLMClient:
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
async with self._client.stream(
|
||||
"POST", "/v1/chat/completions", json=payload
|
||||
) as response:
|
||||
async with self._client.stream("POST", "/v1/chat/completions", json=payload) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
async for line in response.aiter_lines():
|
||||
@@ -143,6 +142,7 @@ class LLMClient:
|
||||
break
|
||||
|
||||
import json
|
||||
|
||||
chunk = json.loads(data)
|
||||
delta = chunk["choices"][0].get("delta", {})
|
||||
content = delta.get("content", "")
|
||||
@@ -163,21 +163,25 @@ class LLMClient:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
elif context:
|
||||
# Default RAG system prompt
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You are a helpful assistant. Use the provided context to answer "
|
||||
"the user's question. If the context doesn't contain relevant "
|
||||
"information, say so."
|
||||
),
|
||||
})
|
||||
messages.append(
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You are a helpful assistant. Use the provided context to answer "
|
||||
"the user's question. If the context doesn't contain relevant "
|
||||
"information, say so."
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Add context as a separate message if provided
|
||||
if context:
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": f"Context:\n{context}\n\nQuestion: {prompt}",
|
||||
})
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Context:\n{context}\n\nQuestion: {prompt}",
|
||||
}
|
||||
)
|
||||
else:
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
Milvus vector database client.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional, Any
|
||||
|
||||
from pymilvus import connections, Collection, utility
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from pymilvus import Collection, connections, utility
|
||||
|
||||
from handler_base.config import Settings
|
||||
from handler_base.telemetry import create_span
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Reranker service client (Infinity/BGE Reranker).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
@@ -73,11 +74,13 @@ class RerankerClient:
|
||||
enriched = []
|
||||
for r in results:
|
||||
idx = r.get("index", 0)
|
||||
enriched.append({
|
||||
"index": idx,
|
||||
"score": r.get("relevance_score", r.get("score", 0)),
|
||||
"document": documents[idx] if idx < len(documents) else "",
|
||||
})
|
||||
enriched.append(
|
||||
{
|
||||
"index": idx,
|
||||
"score": r.get("relevance_score", r.get("score", 0)),
|
||||
"document": documents[idx] if idx < len(documents) else "",
|
||||
}
|
||||
)
|
||||
|
||||
return enriched
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
STT service client (Whisper/faster-whisper).
|
||||
"""
|
||||
import io
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
TTS service client (Coqui XTTS).
|
||||
"""
|
||||
import io
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ Configuration management using Pydantic Settings.
|
||||
|
||||
Environment variables are automatically loaded and validated.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Base handler class for building NATS-based services.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import signal
|
||||
@@ -12,7 +13,7 @@ from nats.aio.msg import Msg
|
||||
from handler_base.config import Settings
|
||||
from handler_base.health import HealthServer
|
||||
from handler_base.nats_client import NATSClient
|
||||
from handler_base.telemetry import setup_telemetry, create_span
|
||||
from handler_base.telemetry import create_span, setup_telemetry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -178,7 +179,7 @@ class Handler(ABC):
|
||||
# Wait for shutdown signal
|
||||
await self._shutdown_event.wait()
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception("Fatal error in handler")
|
||||
raise
|
||||
finally:
|
||||
|
||||
@@ -3,12 +3,13 @@ HTTP health check server.
|
||||
|
||||
Provides /health and /ready endpoints for Kubernetes probes.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Callable, Optional, Awaitable
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
import threading
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Awaitable, Callable, Optional
|
||||
|
||||
from handler_base.config import Settings
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
NATS client wrapper with connection management and utilities.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import logging
|
||||
from typing import Any, Callable, Optional, Awaitable
|
||||
from typing import Any, Awaitable, Callable, Optional
|
||||
|
||||
import msgpack
|
||||
import nats
|
||||
@@ -129,6 +129,7 @@ class NATSClient:
|
||||
payload = msgpack.packb(data, use_bin_type=True)
|
||||
else:
|
||||
import json
|
||||
|
||||
payload = json.dumps(data).encode()
|
||||
|
||||
await self.nc.publish(subject, payload)
|
||||
@@ -162,6 +163,7 @@ class NATSClient:
|
||||
payload = msgpack.packb(data, use_bin_type=True)
|
||||
else:
|
||||
import json
|
||||
|
||||
payload = json.dumps(data).encode()
|
||||
|
||||
response = await self.nc.request(subject, payload, timeout=timeout)
|
||||
@@ -170,6 +172,7 @@ class NATSClient:
|
||||
return msgpack.unpackb(response.data, raw=False)
|
||||
else:
|
||||
import json
|
||||
|
||||
return json.loads(response.data.decode())
|
||||
|
||||
@staticmethod
|
||||
@@ -181,4 +184,5 @@ class NATSClient:
|
||||
def decode_json(msg: Msg) -> Any:
|
||||
"""Decode a JSON message."""
|
||||
import json
|
||||
|
||||
return json.loads(msg.data.decode())
|
||||
|
||||
@@ -3,26 +3,27 @@ OpenTelemetry setup for tracing and metrics.
|
||||
|
||||
Supports both gRPC and HTTP exporters, with optional HyperDX integration.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from opentelemetry import trace, metrics
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.sdk.metrics import MeterProvider
|
||||
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry import metrics, trace
|
||||
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
||||
OTLPSpanExporter as OTLPSpanExporterHTTP,
|
||||
)
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
||||
OTLPMetricExporter as OTLPMetricExporterHTTP,
|
||||
)
|
||||
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION, SERVICE_NAMESPACE
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
||||
OTLPSpanExporter as OTLPSpanExporterHTTP,
|
||||
)
|
||||
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
||||
from opentelemetry.instrumentation.logging import LoggingInstrumentor
|
||||
from opentelemetry.sdk.metrics import MeterProvider
|
||||
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
||||
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_NAMESPACE, SERVICE_VERSION, Resource
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
|
||||
from handler_base.config import Settings
|
||||
|
||||
@@ -60,13 +61,15 @@ def setup_telemetry(
|
||||
return None, None
|
||||
|
||||
# Create resource with service information
|
||||
resource = Resource.create({
|
||||
SERVICE_NAME: settings.service_name,
|
||||
SERVICE_VERSION: settings.service_version,
|
||||
SERVICE_NAMESPACE: settings.service_namespace,
|
||||
"deployment.environment": settings.deployment_env,
|
||||
"host.name": os.environ.get("HOSTNAME", "unknown"),
|
||||
})
|
||||
resource = Resource.create(
|
||||
{
|
||||
SERVICE_NAME: settings.service_name,
|
||||
SERVICE_VERSION: settings.service_version,
|
||||
SERVICE_NAMESPACE: settings.service_namespace,
|
||||
"deployment.environment": settings.deployment_env,
|
||||
"host.name": os.environ.get("HOSTNAME", "unknown"),
|
||||
}
|
||||
)
|
||||
|
||||
# Determine endpoint and exporter type
|
||||
if settings.hyperdx_enabled and settings.hyperdx_api_key:
|
||||
@@ -150,5 +153,6 @@ def create_span(name: str, **kwargs):
|
||||
if _tracer is None:
|
||||
# Return a no-op context manager
|
||||
from contextlib import nullcontext
|
||||
|
||||
return nullcontext()
|
||||
return _tracer.start_as_current_span(name, **kwargs)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"""
|
||||
Pytest configuration and fixtures.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# Set test environment variables before importing handler_base
|
||||
os.environ.setdefault("NATS_URL", "nats://localhost:4222")
|
||||
os.environ.setdefault("REDIS_URL", "redis://localhost:6379")
|
||||
@@ -29,6 +28,7 @@ def event_loop():
|
||||
def settings():
|
||||
"""Create test settings."""
|
||||
from handler_base.config import Settings
|
||||
|
||||
return Settings(
|
||||
service_name="test-service",
|
||||
service_version="1.0.0-test",
|
||||
@@ -56,7 +56,7 @@ def mock_nats_message():
|
||||
msg = MagicMock()
|
||||
msg.subject = "test.subject"
|
||||
msg.reply = "test.reply"
|
||||
msg.data = b'\x82\xa8query\xa5hello\xaarequest_id\xa4test' # msgpack
|
||||
msg.data = b"\x82\xa8query\xa5hello\xaarequest_id\xa4test" # msgpack
|
||||
return msg
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""
|
||||
Unit tests for service clients.
|
||||
"""
|
||||
import json
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
||||
class TestEmbeddingsClient:
|
||||
@@ -23,9 +24,7 @@ class TestEmbeddingsClient:
|
||||
"""Test embedding a single text."""
|
||||
# Setup mock response
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"data": [{"embedding": sample_embedding, "index": 0}]
|
||||
}
|
||||
mock_response.json.return_value = {"data": [{"embedding": sample_embedding, "index": 0}]}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.post.return_value = mock_response
|
||||
|
||||
@@ -118,10 +117,8 @@ class TestLLMClient:
|
||||
"""Test generating a response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"choices": [
|
||||
{"message": {"content": "Hello! I'm an AI assistant."}}
|
||||
],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 20}
|
||||
"choices": [{"message": {"content": "Hello! I'm an AI assistant."}}],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 20},
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.post.return_value = mock_response
|
||||
@@ -135,17 +132,14 @@ class TestLLMClient:
|
||||
"""Test generating with RAG context."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"choices": [
|
||||
{"message": {"content": "Based on the context..."}}
|
||||
],
|
||||
"usage": {}
|
||||
"choices": [{"message": {"content": "Based on the context..."}}],
|
||||
"usage": {},
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.post.return_value = mock_response
|
||||
|
||||
result = await llm_client.generate(
|
||||
"What is Python?",
|
||||
context="Python is a programming language."
|
||||
"What is Python?", context="Python is a programming language."
|
||||
)
|
||||
|
||||
assert "Based on the context" in result
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""
|
||||
Unit tests for handler_base.config module.
|
||||
"""
|
||||
import os
|
||||
import pytest
|
||||
|
||||
|
||||
class TestSettings:
|
||||
@@ -22,6 +20,7 @@ class TestSettings:
|
||||
|
||||
# Need to reimport to pick up env changes
|
||||
from handler_base.config import Settings
|
||||
|
||||
s = Settings()
|
||||
|
||||
assert s.service_name == "env-service"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""
|
||||
Unit tests for handler_base.health module.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from http.client import HTTPConnection
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestHealthServer:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""
|
||||
Unit tests for handler_base.nats_client module.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import msgpack
|
||||
import pytest
|
||||
|
||||
|
||||
class TestNATSClient:
|
||||
@@ -13,6 +15,7 @@ class TestNATSClient:
|
||||
def nats_client(self, settings):
|
||||
"""Create a NATSClient instance."""
|
||||
from handler_base.nats_client import NATSClient
|
||||
|
||||
return NATSClient(settings)
|
||||
|
||||
def test_init(self, nats_client, settings):
|
||||
@@ -35,6 +38,7 @@ class TestNATSClient:
|
||||
def test_decode_json(self, nats_client):
|
||||
"""Test JSON decoding."""
|
||||
import json
|
||||
|
||||
data = {"query": "hello"}
|
||||
|
||||
msg = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user