ai-robot-core/ai-service/app/services/llm/base.py

177 lines
4.9 KiB
Python
Raw Normal View History

"""
Base LLM client interface.
[AC-AISVC-02, AC-AISVC-06] Abstract interface for LLM providers.
Design reference: design.md Section 8.1 - LLMClient interface
- generate(prompt, params) -> text
- stream_generate(prompt, params) -> iterator[delta]
"""
from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator
from dataclasses import dataclass, field
from typing import Any
@dataclass
class LLMConfig:
"""
Configuration for LLM client.
[AC-AISVC-02] Supports configurable model parameters.
"""
model: str = "gpt-4o-mini"
max_tokens: int = 2048
temperature: float = 0.7
top_p: float = 1.0
timeout_seconds: int = 30
max_retries: int = 3
extra_params: dict[str, Any] = field(default_factory=dict)
@dataclass
class ToolCall:
"""
Represents a function call from the LLM.
Used in Function Calling mode.
"""
id: str
name: str
arguments: dict[str, Any]
def to_dict(self) -> dict[str, Any]:
import json
return {
"id": self.id,
"type": "function",
"function": {
"name": self.name,
"arguments": json.dumps(self.arguments, ensure_ascii=False),
}
}
@dataclass
class LLMResponse:
"""
Response from LLM generation.
[AC-AISVC-02] Contains generated content and metadata.
"""
content: str | None = None
model: str = ""
usage: dict[str, int] = field(default_factory=dict)
finish_reason: str = "stop"
tool_calls: list[ToolCall] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
@property
def has_tool_calls(self) -> bool:
"""Check if response contains tool calls."""
return len(self.tool_calls) > 0
@dataclass
class LLMStreamChunk:
"""
Streaming chunk from LLM.
[AC-AISVC-06, AC-AISVC-07] Incremental output for SSE streaming.
"""
delta: str
model: str
finish_reason: str | None = None
tool_calls_delta: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass
class ToolDefinition:
"""
Tool definition for Function Calling.
Compatible with OpenAI/DeepSeek function calling format.
"""
name: str
description: str
parameters: dict[str, Any]
type: str = "function"
def to_openai_format(self) -> dict[str, Any]:
"""Convert to OpenAI tools format."""
return {
"type": self.type,
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters,
}
}
class LLMClient(ABC):
"""
Abstract base class for LLM clients.
[AC-AISVC-02, AC-AISVC-06] Provides unified interface for different LLM providers.
Design reference: design.md Section 8.2 - Plugin points
- OpenAICompatibleClient / LocalModelClient can be swapped
"""
@abstractmethod
async def generate(
self,
messages: list[dict[str, Any]],
config: LLMConfig | None = None,
tools: list[ToolDefinition] | None = None,
tool_choice: str | dict[str, Any] | None = None,
**kwargs: Any,
) -> LLMResponse:
"""
Generate a non-streaming response.
[AC-AISVC-02] Returns complete response for ChatResponse.
Args:
messages: List of chat messages with 'role' and 'content'.
config: Optional LLM configuration overrides.
tools: Optional list of tools for function calling.
tool_choice: Tool choice strategy ("auto", "none", or specific tool).
**kwargs: Additional provider-specific parameters.
Returns:
LLMResponse with generated content, tool_calls, and metadata.
Raises:
LLMException: If generation fails.
"""
pass
@abstractmethod
async def stream_generate(
self,
messages: list[dict[str, Any]],
config: LLMConfig | None = None,
tools: list[ToolDefinition] | None = None,
tool_choice: str | dict[str, Any] | None = None,
**kwargs: Any,
) -> AsyncGenerator[LLMStreamChunk, None]:
"""
Generate a streaming response.
[AC-AISVC-06, AC-AISVC-07] Yields incremental chunks for SSE.
Args:
messages: List of chat messages with 'role' and 'content'.
config: Optional LLM configuration overrides.
tools: Optional list of tools for function calling.
tool_choice: Tool choice strategy ("auto", "none", or specific tool).
**kwargs: Additional provider-specific parameters.
Yields:
LLMStreamChunk with incremental content.
Raises:
LLMException: If generation fails.
"""
pass
@abstractmethod
async def close(self) -> None:
"""Close the client and release resources."""
pass