feat: add AskSage LLM provider integration

Implement AskSageAdapter using the official asksageclient SDK to support
AskSage as an LLM provider option. This enables users to leverage
AskSage's API with configurable email, API key, and model settings.

- Add AskSageAdapter class with async support via thread pool
- Update Settings to include asksage_email, asksage_api_key, asksage_model
- Extend llm_mode literal to include "asksage" option
- Update dependency injection to instantiate AskSageAdapter when configured
- Remove completed OPENAI_INTEGRATION_PLAN.md
- Update requirements.txt with full dependency list including asksageclient
This commit is contained in:
Danny
2026-01-16 12:16:34 -06:00
parent ad5f8ef798
commit 4c084d7668
6 changed files with 116 additions and 173 deletions
+6 -1
View File
@@ -6,7 +6,7 @@ from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Application configuration loaded from environment variables."""
llm_mode: Literal["local", "remote", "openai"] = "local"
llm_mode: Literal["local", "remote", "openai", "asksage"] = "local"
llm_remote_url: str = ""
llm_remote_token: str = ""
@@ -14,6 +14,11 @@ class Settings(BaseSettings):
openai_api_key: str = ""
openai_model: str = "gpt-4o-mini"
# AskSage configuration
asksage_email: str = ""
asksage_api_key: str = ""
asksage_model: str = "gpt-4o"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
+70
View File
@@ -1,10 +1,12 @@
"""LLM adapter implementations with FastAPI dependency injection support."""
import asyncio
from abc import ABC, abstractmethod
from functools import lru_cache
from typing import Annotated
import httpx
from asksageclient import AskSageClient
from fastapi import Depends
from openai import (
AsyncOpenAI,
@@ -182,6 +184,63 @@ class OpenAIAdapter(LLMAdapter):
)
class AskSageAdapter(LLMAdapter):
"""AskSage API adapter using the official asksageclient SDK."""
def __init__(self, email: str, api_key: str, model: str = "gpt-4o"):
"""Initialize the AskSage adapter.
Args:
email: AskSage account email
api_key: AskSage API key
model: Model identifier (e.g., "gpt-4o", "claude-3-opus")
"""
self.client = AskSageClient(email=email, api_key=api_key)
self.model = model
async def generate(self, conversation_id: str, message: str) -> str:
"""Generate a response using the AskSage API.
Args:
conversation_id: The conversation identifier (for future use)
message: The user's message
Returns:
The generated response string
Raises:
LLMAuthenticationError: If credentials are invalid
LLMConnectionError: If connection fails
LLMResponseError: If response content is empty or invalid
LLMError: For other API errors
"""
try:
# AskSageClient is synchronous, run in thread pool to avoid blocking
response = await asyncio.to_thread(
self.client.query,
message=message,
model=self.model,
)
if not isinstance(response, dict):
raise LLMResponseError("AskSage returned invalid response format")
content = response.get("response")
if content is None:
raise LLMResponseError("AskSage returned empty response content")
return content
except LLMError:
raise
except Exception as e:
error_msg = str(e).lower()
if "auth" in error_msg or "401" in error_msg or "unauthorized" in error_msg:
raise LLMAuthenticationError(f"AskSage authentication failed: {e}")
if "connect" in error_msg or "timeout" in error_msg:
raise LLMConnectionError(f"Could not connect to AskSage: {e}")
raise LLMError(f"AskSage API error: {e}")
# --- Dependency Injection Support ---
@@ -225,6 +284,17 @@ def get_adapter(settings: Annotated[Settings, Depends(get_settings)]) -> LLMAdap
token=settings.llm_remote_token or None,
)
if settings.llm_mode == "asksage":
if not settings.asksage_email or not settings.asksage_api_key:
raise LLMConfigurationError(
"ASKSAGE_EMAIL and ASKSAGE_API_KEY must be set when LLM_MODE is 'asksage'"
)
return AskSageAdapter(
email=settings.asksage_email,
api_key=settings.asksage_api_key,
model=settings.asksage_model,
)
return LocalAdapter()
+1 -1
View File
@@ -17,7 +17,7 @@ class ChatResponse(BaseModel):
conversation_id: str = Field(..., description="Conversation ID (generated if not provided)")
response: str = Field(..., description="The LLM's response")
mode: Literal["local", "remote", "openai"] = Field(..., description="Which adapter was used")
mode: Literal["local", "remote", "openai", "asksage"] = Field(..., description="Which adapter was used")
sources: list = Field(default_factory=list, description="Source references (empty for now)")