feat: Initial commit of LLM Tool Proxy
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
85
tests/test_main.py
Normal file
85
tests/test_main.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
import json
|
||||
|
||||
# The TestClient allows us to make requests to our FastAPI app without a running server.
|
||||
client = TestClient(app)
|
||||
|
||||
def test_root_endpoint():
|
||||
"""Tests the health check endpoint."""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "LLM Tool Proxy is running."}
|
||||
|
||||
def test_chat_completions_no_tools(monkeypatch):
|
||||
"""
|
||||
Tests the main endpoint with a simple request that does not include tools.
|
||||
This is now an INTEGRATION TEST against the live backend.
|
||||
"""
|
||||
monkeypatch.setenv("REAL_LLM_API_URL", "https://qwapi.oopsapi.com/v1/chat/completions")
|
||||
monkeypatch.setenv("REAL_LLM_API_KEY", "dummy-key")
|
||||
|
||||
request_data = {
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello there!"}
|
||||
]
|
||||
}
|
||||
response = client.post("/v1/chat/completions", json=request_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
response_json = response.json()
|
||||
|
||||
# Assertions for a real response: check structure and types, not specific content.
|
||||
assert "message" in response_json
|
||||
assert response_json["message"]["role"] == "assistant"
|
||||
# The real LLM should return some content
|
||||
assert isinstance(response_json["message"]["content"], str)
|
||||
assert len(response_json["message"]["content"]) > 0
|
||||
|
||||
|
||||
def test_chat_completions_with_tools_integration(monkeypatch):
|
||||
"""
|
||||
Tests the main endpoint with a request that includes tools against the live backend.
|
||||
We check for a valid response, but cannot guarantee a tool will be called.
|
||||
"""
|
||||
monkeypatch.setenv("REAL_LLM_API_URL", "https://qwapi.oopsapi.com/v1/chat/completions")
|
||||
monkeypatch.setenv("REAL_LLM_API_KEY", "dummy-key")
|
||||
|
||||
request_data = {
|
||||
"messages": [
|
||||
{"role": "user", "content": "What's the weather in San Francisco?"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get the current weather for a specified city",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {"type": "string", "description": "The city name"}
|
||||
},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = client.post("/v1/chat/completions", json=request_data)
|
||||
|
||||
# For an integration test, the main goal is to ensure our proxy
|
||||
# communicates successfully and can parse the response without errors.
|
||||
assert response.status_code == 200
|
||||
response_json = response.json()
|
||||
|
||||
# We assert that the basic structure is correct.
|
||||
assert "message" in response_json
|
||||
assert response_json["message"]["role"] == "assistant"
|
||||
|
||||
# The response might contain content, a tool_call, or both. We just
|
||||
# ensure the response fits our Pydantic model, which the TestClient handles.
|
||||
# A successful 200 response is our primary success metric here.
|
||||
assert response_json is not None
|
||||
|
||||
96
tests/test_services.py
Normal file
96
tests/test_services.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import pytest
|
||||
import httpx
|
||||
import json
|
||||
from typing import List, AsyncGenerator
|
||||
|
||||
from app.services import call_llm_api_real
|
||||
from app.models import ChatMessage
|
||||
from app.core.config import Settings
|
||||
|
||||
# Sample SSE chunks to simulate a streaming response
|
||||
SSE_STREAM_CHUNKS = [
|
||||
'data: {"choices": [{"delta": {"role": "assistant", "content": "Hello"}}]}',
|
||||
'data: {"choices": [{"delta": {"content": " world!"}}]}',
|
||||
'data: {"choices": [{"delta": {"tool_calls": [{"index": 0, "id": "call_123", "function": {"name": "get_weather", "arguments": ""}}]}}]}',
|
||||
'data: {"choices": [{"delta": {"tool_calls": [{"index": 0, "function": {"arguments": "{\\"location\\":"}}]}}]}',
|
||||
'data: {"choices": [{"delta": {"tool_calls": [{"index": 0, "function": {"arguments": " \\"San Francisco\\"}"}}]}}]}',
|
||||
'data: [DONE]',
|
||||
]
|
||||
|
||||
# Mock settings for the test
|
||||
@pytest.fixture
|
||||
def mock_settings() -> Settings:
|
||||
"""Provides mock settings for tests."""
|
||||
return Settings(
|
||||
REAL_LLM_API_URL="http://fake-llm-api.com/chat",
|
||||
REAL_LLM_API_KEY="fake-key"
|
||||
)
|
||||
|
||||
# Async generator to mock the streaming response
|
||||
async def mock_aiter_lines() -> AsyncGenerator[str, None]:
|
||||
for chunk in SSE_STREAM_CHUNKS:
|
||||
yield chunk
|
||||
|
||||
# Mock for the httpx.Response object
|
||||
class MockStreamResponse:
|
||||
def __init__(self, status_code: int = 200):
|
||||
self._status_code = status_code
|
||||
|
||||
def raise_for_status(self):
|
||||
if self._status_code != 200:
|
||||
raise httpx.HTTPStatusError(
|
||||
message="Error", request=httpx.Request("POST", ""), response=httpx.Response(self._status_code)
|
||||
)
|
||||
|
||||
def aiter_lines(self) -> AsyncGenerator[str, None]:
|
||||
return mock_aiter_lines()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
|
||||
# Mock for the httpx.AsyncClient
|
||||
class MockAsyncClient:
|
||||
def stream(self, method, url, headers, json, timeout):
|
||||
return MockStreamResponse()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_call_llm_api_real_streaming(monkeypatch, mock_settings):
|
||||
"""
|
||||
Tests that `call_llm_api_real` correctly handles an SSE stream,
|
||||
parses the chunks, and assembles the final response message.
|
||||
"""
|
||||
# Patch httpx.AsyncClient to use our mock
|
||||
monkeypatch.setattr(httpx, "AsyncClient", MockAsyncClient)
|
||||
|
||||
messages = [ChatMessage(role="user", content="What is the weather in San Francisco?")]
|
||||
|
||||
# Call the function
|
||||
result = await call_llm_api_real(messages, mock_settings)
|
||||
|
||||
# Define the expected assembled result
|
||||
expected_result = {
|
||||
"role": "assistant",
|
||||
"content": "Hello world!",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"location": "San Francisco"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Assert that the result matches the expected output
|
||||
assert result == expected_result
|
||||
Reference in New Issue
Block a user