donkit-llm 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,256 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import Enum, Flag, auto
3
+ from typing import Any, AsyncIterator, Literal
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class ModelCapability(Flag):
9
+ """Capabilities that a model can support."""
10
+
11
+ TEXT_GENERATION = auto()
12
+ STREAMING = auto()
13
+ MULTIMODAL_INPUT = auto() # Images, files, audio
14
+ STRUCTURED_OUTPUT = auto() # JSON schema / response format
15
+ TOOL_CALLING = auto() # Function calling
16
+ EMBEDDINGS = auto()
17
+ VISION = auto() # Specifically image understanding
18
+ AUDIO_INPUT = auto()
19
+ FILE_INPUT = auto()
20
+
21
+
22
+ class ContentType(str, Enum):
23
+ """Types of content that can be included in messages."""
24
+
25
+ TEXT = "text"
26
+ IMAGE_URL = "image_url"
27
+ IMAGE_BASE64 = "image_base64"
28
+ FILE_URL = "file_url"
29
+ FILE_BASE64 = "file_base64"
30
+ AUDIO_URL = "audio_url"
31
+ AUDIO_BASE64 = "audio_base64"
32
+
33
+
34
+ class ContentPart(BaseModel):
35
+ """A single part of message content (text, image, file, etc.)."""
36
+
37
+ type: ContentType
38
+ content: str # Text, URL, or base64 data
39
+ mime_type: str | None = None # For files/images/audio
40
+ metadata: dict[str, Any] | None = None
41
+
42
+
43
+ class Message(BaseModel):
44
+ """A single message in a conversation."""
45
+
46
+ role: Literal["system", "user", "assistant", "tool"]
47
+ content: str | list[ContentPart] # Simple text or multimodal parts
48
+ name: str | None = None # For tool/function messages
49
+ tool_call_id: str | None = None # For tool response messages
50
+ tool_calls: list["ToolCall"] | None = None # For assistant requesting tools
51
+
52
+
53
+ class ToolCall(BaseModel):
54
+ """A tool/function call requested by the model."""
55
+
56
+ id: str
57
+ type: Literal["function"] = "function"
58
+ function: "FunctionCall"
59
+
60
+
61
+ class FunctionCall(BaseModel):
62
+ """Details of a function call."""
63
+
64
+ name: str
65
+ arguments: str # JSON string of arguments
66
+
67
+
68
+ class Tool(BaseModel):
69
+ """Definition of a tool/function available to the model."""
70
+
71
+ type: Literal["function"] = "function"
72
+ function: "FunctionDefinition"
73
+
74
+
75
+ class FunctionDefinition(BaseModel):
76
+ """Definition of a function that can be called."""
77
+
78
+ name: str
79
+ description: str
80
+ parameters: dict[str, Any] # JSON Schema for parameters
81
+ strict: bool | None = None # For strict schema adherence (OpenAI)
82
+
83
+
84
+ class GenerateRequest(BaseModel):
85
+ """Request for text generation."""
86
+
87
+ messages: list[Message]
88
+ temperature: float | None = None
89
+ max_tokens: int | None = None
90
+ top_p: float | None = None
91
+ stop: list[str] | None = None
92
+ tools: list[Tool] | None = None # Available tools for function calling
93
+ tool_choice: str | dict[str, Any] | None = None # "auto", "none", or specific tool
94
+ response_format: dict[str, Any] | None = None # For structured output (JSON schema)
95
+ stream: bool = False
96
+ metadata: dict[str, Any] | None = None
97
+
98
+
99
+ class GenerateResponse(BaseModel):
100
+ """Response from text generation."""
101
+
102
+ content: str | None = None # Generated text
103
+ tool_calls: list[ToolCall] | None = None # Requested tool calls
104
+ finish_reason: str | None = None # "stop", "length", "tool_calls", etc.
105
+ usage: dict[str, int | None] | None = None # Token usage stats
106
+ metadata: dict[str, Any] | None = None
107
+
108
+
109
+ class StreamChunk(BaseModel):
110
+ """A chunk of streamed response."""
111
+
112
+ content: str | None = None
113
+ tool_calls: list[ToolCall] | None = None # Partial or complete tool calls
114
+ finish_reason: str | None = None
115
+ metadata: dict[str, Any] | None = None
116
+
117
+
118
+ class EmbeddingRequest(BaseModel):
119
+ """Request for embeddings."""
120
+
121
+ input: str | list[str] # Text(s) to embed
122
+ dimensions: int | None = None # Output dimensionality (if supported)
123
+ metadata: dict[str, Any] | None = None
124
+
125
+
126
+ class EmbeddingResponse(BaseModel):
127
+ """Response from embedding generation."""
128
+
129
+ embeddings: list[list[float]] # List of embedding vectors
130
+ usage: dict[str, int] | None = None
131
+ metadata: dict[str, Any] | None = None
132
+
133
+
134
+ class LLMModelAbstract(ABC):
135
+ """
136
+ Abstract base class for all LLM model providers.
137
+
138
+ Provides a unified interface for:
139
+ - Text generation (sync and streaming)
140
+ - Multimodal input (images, files, audio)
141
+ - Structured output (JSON schema)
142
+ - Tool/function calling
143
+ - Embeddings
144
+
145
+ Not all models support all capabilities. Use `supports_capability()` to check.
146
+ """
147
+
148
+ @property
149
+ @abstractmethod
150
+ def model_name(self) -> str:
151
+ """Return the model name/identifier."""
152
+ pass
153
+
154
+ @property
155
+ @abstractmethod
156
+ def capabilities(self) -> ModelCapability:
157
+ """Return the capabilities supported by this model."""
158
+ pass
159
+
160
+ def supports_capability(self, capability: ModelCapability) -> bool:
161
+ """Check if this model supports a specific capability."""
162
+ return bool(self.capabilities & capability)
163
+
164
+ @abstractmethod
165
+ async def generate(self, request: GenerateRequest) -> GenerateResponse:
166
+ """
167
+ Generate a response for the given request.
168
+
169
+ Args:
170
+ request: The generation request with messages, tools, etc.
171
+
172
+ Returns:
173
+ The generated response with content and/or tool calls.
174
+
175
+ Raises:
176
+ NotImplementedError: If the model doesn't support text generation.
177
+ ValueError: If request contains unsupported features.
178
+ """
179
+ pass
180
+
181
+ @abstractmethod
182
+ async def generate_stream(
183
+ self, request: GenerateRequest
184
+ ) -> AsyncIterator[StreamChunk]:
185
+ """
186
+ Generate a streaming response for the given request.
187
+
188
+ Args:
189
+ request: The generation request with messages, tools, etc.
190
+
191
+ Yields:
192
+ Chunks of the generated response.
193
+
194
+ Raises:
195
+ NotImplementedError: If the model doesn't support streaming.
196
+ ValueError: If request contains unsupported features.
197
+ """
198
+ pass
199
+
200
+ async def embed(self, request: EmbeddingRequest) -> EmbeddingResponse:
201
+ """
202
+ Generate embeddings for the given input.
203
+
204
+ Args:
205
+ request: The embedding request with input text(s).
206
+
207
+ Returns:
208
+ The embedding vectors.
209
+
210
+ Raises:
211
+ NotImplementedError: If the model doesn't support embeddings.
212
+ """
213
+ raise NotImplementedError(
214
+ f"Model {self.model_name} does not support embeddings"
215
+ )
216
+
217
+ async def validate_request(self, request: GenerateRequest) -> None:
218
+ """
219
+ Validate that the request is compatible with this model's capabilities.
220
+
221
+ Args:
222
+ request: The generation request to validate.
223
+
224
+ Raises:
225
+ ValueError: If request contains unsupported features.
226
+ """
227
+ # Check multimodal input
228
+ has_multimodal = any(isinstance(msg.content, list) for msg in request.messages)
229
+ if has_multimodal and not self.supports_capability(
230
+ ModelCapability.MULTIMODAL_INPUT
231
+ ):
232
+ raise ValueError(
233
+ f"Model {self.model_name} does not support multimodal input"
234
+ )
235
+
236
+ # Check tool calling
237
+ if request.tools and not self.supports_capability(ModelCapability.TOOL_CALLING):
238
+ raise ValueError(f"Model {self.model_name} does not support tool calling")
239
+
240
+ # Check structured output
241
+ if request.response_format and not self.supports_capability(
242
+ ModelCapability.STRUCTURED_OUTPUT
243
+ ):
244
+ raise ValueError(
245
+ f"Model {self.model_name} does not support structured output"
246
+ )
247
+
248
+ # Check streaming
249
+ if request.stream and not self.supports_capability(ModelCapability.STREAMING):
250
+ raise ValueError(f"Model {self.model_name} does not support streaming")
251
+
252
+
253
+ # Update forward references for Pydantic models
254
+ Message.model_rebuild()
255
+ ToolCall.model_rebuild()
256
+ Tool.model_rebuild()