Add LLM token
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
"""Provide service-layer logic for base client."""
|
||||
"""Provide service-layer logic for base client.
|
||||
|
||||
P0-0: ``LLMResponse`` now carries an optional ``tool_calls`` list so that any
|
||||
downstream code (agents, pipelines) can inspect and dispatch tool invocations
|
||||
without touching the provider-specific adapter layer.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Optional, Any
|
||||
from enum import Enum
|
||||
|
||||
from app.services.llm.tool_types import Tool, ToolCall # noqa: F401 – re-exported for callers
|
||||
# Keep provider-specific behavior explicit so debugging stays straightforward.
|
||||
|
||||
|
||||
@@ -24,6 +31,8 @@ class LLMResponse:
|
||||
finish_reason: str = "stop"
|
||||
latency_ms: int = 0
|
||||
error: Optional[str] = None
|
||||
# P0-0: populated when the model returns tool-call(s) instead of plain text.
|
||||
tool_calls: List[ToolCall] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def is_success(self) -> bool:
|
||||
@@ -63,9 +72,19 @@ class BaseLLMClient(ABC):
|
||||
messages: List[Dict[str, str]],
|
||||
max_tokens: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
tools: Optional[List["Tool"]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Handle chat for the Base L L M Client instance."""
|
||||
"""Handle chat for the Base L L M Client instance.
|
||||
|
||||
Args:
|
||||
messages: OpenAI-format message list.
|
||||
max_tokens: Override config max_tokens when set.
|
||||
temperature: Override config temperature when set.
|
||||
tools: Optional list of Tool definitions to offer the model.
|
||||
When provided, the model may respond with tool_calls in the
|
||||
returned LLMResponse instead of (or in addition to) content.
|
||||
"""
|
||||
pass
|
||||
|
||||
def complete(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Provide service-layer logic for deepseek client."""
|
||||
"""Provide service-layer logic for deepseek client.
|
||||
|
||||
P0-0: ``chat()`` now accepts an optional ``tools`` list and parses ``tool_calls``
|
||||
from the model response so that callers can dispatch tool invocations.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import List, Dict, Optional
|
||||
@@ -6,6 +10,7 @@ from loguru import logger
|
||||
import httpx
|
||||
|
||||
from .base_client import BaseLLMClient, LLMResponse, LLMConfig, LLMProvider
|
||||
from .tool_types import Tool, ToolCall
|
||||
# Keep provider-specific behavior explicit so debugging stays straightforward.
|
||||
|
||||
|
||||
@@ -46,13 +51,20 @@ class DeepSeekClient(BaseLLMClient):
|
||||
messages: List[Dict[str, str]],
|
||||
max_tokens: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
tools: Optional[List[Tool]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Handle chat for the Deep Seek Client instance."""
|
||||
"""Handle chat for the Deep Seek Client instance.
|
||||
|
||||
When ``tools`` is provided the request includes the tool definitions and
|
||||
``tool_choice="auto"``; any tool_calls returned by the model are parsed
|
||||
into ``LLMResponse.tool_calls``.
|
||||
"""
|
||||
import json
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
payload = {
|
||||
payload: Dict = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens or self.config.max_tokens,
|
||||
@@ -61,6 +73,11 @@ class DeepSeekClient(BaseLLMClient):
|
||||
"stream": False
|
||||
}
|
||||
|
||||
# P0-0: inject tool definitions when provided.
|
||||
if tools:
|
||||
payload["tools"] = [t.to_openai_format() for t in tools]
|
||||
payload["tool_choice"] = "auto"
|
||||
|
||||
response = self._client.post("/chat/completions", json=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -71,12 +88,24 @@ class DeepSeekClient(BaseLLMClient):
|
||||
choices = data.get("choices", [{}])
|
||||
message = choices[0].get("message", {})
|
||||
|
||||
# P0-0: parse tool_calls returned by the model.
|
||||
raw_tool_calls = message.get("tool_calls") or []
|
||||
parsed_tool_calls: List[ToolCall] = []
|
||||
for tc in raw_tool_calls:
|
||||
fn = tc.get("function", {})
|
||||
try:
|
||||
args = json.loads(fn.get("arguments", "{}"))
|
||||
except json.JSONDecodeError:
|
||||
args = {}
|
||||
parsed_tool_calls.append(ToolCall(id=tc.get("id", ""), name=fn.get("name", ""), arguments=args))
|
||||
|
||||
return LLMResponse(
|
||||
content=message.get("content", ""),
|
||||
content=message.get("content", "") or "",
|
||||
model=data.get("model", self.config.model),
|
||||
usage=data.get("usage", {}),
|
||||
finish_reason=choices[0].get("finish_reason", "stop"),
|
||||
latency_ms=latency_ms
|
||||
latency_ms=latency_ms,
|
||||
tool_calls=parsed_tool_calls,
|
||||
)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Provide service-layer logic for qwen client."""
|
||||
"""Provide service-layer logic for qwen client.
|
||||
|
||||
P0-0: ``chat()`` now accepts an optional ``tools`` list and parses ``tool_calls``
|
||||
from the model response so that callers can dispatch tool invocations.
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
@@ -7,6 +11,7 @@ from loguru import logger
|
||||
import httpx
|
||||
|
||||
from .base_client import BaseLLMClient, LLMResponse, LLMConfig, LLMProvider
|
||||
from .tool_types import Tool, ToolCall
|
||||
# Keep provider-specific behavior explicit so debugging stays straightforward.
|
||||
|
||||
|
||||
@@ -54,14 +59,20 @@ class QwenClient(BaseLLMClient):
|
||||
messages: List[Dict[str, str]],
|
||||
max_tokens: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
tools: Optional[List[Tool]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Handle chat for the Qwen Client instance."""
|
||||
"""Handle chat for the Qwen Client instance.
|
||||
|
||||
When ``tools`` is provided the request includes the tool definitions and
|
||||
``tool_choice="auto"``; any tool_calls returned by the model are parsed
|
||||
into ``LLMResponse.tool_calls``.
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Keep provider-specific behavior explicit so debugging stays straightforward.
|
||||
payload = {
|
||||
payload: Dict = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens or self.config.max_tokens,
|
||||
@@ -70,6 +81,11 @@ class QwenClient(BaseLLMClient):
|
||||
"stream": False
|
||||
}
|
||||
|
||||
# P0-0: inject tool definitions when provided.
|
||||
if tools:
|
||||
payload["tools"] = [t.to_openai_format() for t in tools]
|
||||
payload["tool_choice"] = "auto"
|
||||
|
||||
# Keep provider-specific behavior explicit so debugging stays straightforward.
|
||||
response = self._client.post("/chat/completions", json=payload)
|
||||
response.raise_for_status()
|
||||
@@ -82,12 +98,24 @@ class QwenClient(BaseLLMClient):
|
||||
choices = data.get("choices", [{}])
|
||||
message = choices[0].get("message", {})
|
||||
|
||||
# P0-0: parse tool_calls returned by the model.
|
||||
raw_tool_calls = message.get("tool_calls") or []
|
||||
parsed_tool_calls: List[ToolCall] = []
|
||||
for tc in raw_tool_calls:
|
||||
fn = tc.get("function", {})
|
||||
try:
|
||||
args = json.loads(fn.get("arguments", "{}"))
|
||||
except json.JSONDecodeError:
|
||||
args = {}
|
||||
parsed_tool_calls.append(ToolCall(id=tc.get("id", ""), name=fn.get("name", ""), arguments=args))
|
||||
|
||||
return LLMResponse(
|
||||
content=message.get("content", ""),
|
||||
content=message.get("content", "") or "",
|
||||
model=data.get("model", self.config.model),
|
||||
usage=data.get("usage", {}),
|
||||
finish_reason=choices[0].get("finish_reason", "stop"),
|
||||
latency_ms=latency_ms
|
||||
latency_ms=latency_ms,
|
||||
tool_calls=parsed_tool_calls,
|
||||
)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Shared tool and tool-call type definitions for LLM function calling (P0-0).
|
||||
|
||||
These types implement the OpenAI-compatible tool/function-calling interface so that
|
||||
any provider whose gateway supports the spec (DeepSeek, Qwen, etc.) can expose
|
||||
tools to the LLM and receive structured tool invocations in return.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
"""Represent a single tool invocation returned by the LLM.
|
||||
|
||||
The model fills in ``id``, ``name``, and ``arguments`` when it decides to call
|
||||
a tool instead of (or in addition to) producing a text response.
|
||||
"""
|
||||
|
||||
# Unique identifier assigned by the model for this call.
|
||||
id: str
|
||||
# Name of the tool to invoke, matching the name registered in Tool.
|
||||
name: str
|
||||
# Parsed JSON arguments ready for direct use by the tool handler.
|
||||
arguments: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolParameter:
|
||||
"""JSON-Schema–compatible parameter block for a tool definition."""
|
||||
|
||||
# Top-level schema type — always "object" for OpenAI-compatible tools.
|
||||
type: str = "object"
|
||||
# Map of parameter name → JSON-Schema property descriptor.
|
||||
properties: dict[str, Any] = field(default_factory=dict)
|
||||
# List of required parameter names.
|
||||
required: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tool:
|
||||
"""Describe a callable tool that can be offered to the LLM.
|
||||
|
||||
Example usage::
|
||||
|
||||
search_tool = Tool(
|
||||
name="search_regulations",
|
||||
description="Search the compliance knowledge base for relevant regulation clauses.",
|
||||
parameters=ToolParameter(
|
||||
properties={"query": {"type": "string", "description": "Search query"}},
|
||||
required=["query"],
|
||||
),
|
||||
)
|
||||
response = client.chat(messages, tools=[search_tool])
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
parameters: ToolParameter = field(default_factory=ToolParameter)
|
||||
|
||||
def to_openai_format(self) -> dict[str, Any]:
|
||||
"""Serialise this tool to the OpenAI-compatible function-calling schema.
|
||||
|
||||
The returned dict can be placed directly in the ``tools`` list of a chat
|
||||
completions request without any further transformation.
|
||||
"""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"parameters": {
|
||||
"type": self.parameters.type,
|
||||
"properties": self.parameters.properties,
|
||||
"required": self.parameters.required,
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user