chibi-bot 1.6.0b0__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.
- chibi/__init__.py +0 -0
- chibi/__main__.py +343 -0
- chibi/cli.py +90 -0
- chibi/config/__init__.py +6 -0
- chibi/config/app.py +123 -0
- chibi/config/gpt.py +108 -0
- chibi/config/logging.py +15 -0
- chibi/config/telegram.py +43 -0
- chibi/config_generator.py +233 -0
- chibi/constants.py +362 -0
- chibi/exceptions.py +58 -0
- chibi/models.py +496 -0
- chibi/schemas/__init__.py +0 -0
- chibi/schemas/anthropic.py +20 -0
- chibi/schemas/app.py +54 -0
- chibi/schemas/cloudflare.py +65 -0
- chibi/schemas/mistralai.py +56 -0
- chibi/schemas/suno.py +83 -0
- chibi/service.py +135 -0
- chibi/services/bot.py +276 -0
- chibi/services/lock_manager.py +20 -0
- chibi/services/mcp/manager.py +242 -0
- chibi/services/metrics.py +54 -0
- chibi/services/providers/__init__.py +16 -0
- chibi/services/providers/alibaba.py +79 -0
- chibi/services/providers/anthropic.py +40 -0
- chibi/services/providers/cloudflare.py +98 -0
- chibi/services/providers/constants/suno.py +2 -0
- chibi/services/providers/customopenai.py +11 -0
- chibi/services/providers/deepseek.py +15 -0
- chibi/services/providers/eleven_labs.py +85 -0
- chibi/services/providers/gemini_native.py +489 -0
- chibi/services/providers/grok.py +40 -0
- chibi/services/providers/minimax.py +96 -0
- chibi/services/providers/mistralai_native.py +312 -0
- chibi/services/providers/moonshotai.py +20 -0
- chibi/services/providers/openai.py +74 -0
- chibi/services/providers/provider.py +892 -0
- chibi/services/providers/suno.py +130 -0
- chibi/services/providers/tools/__init__.py +23 -0
- chibi/services/providers/tools/cmd.py +132 -0
- chibi/services/providers/tools/common.py +127 -0
- chibi/services/providers/tools/constants.py +78 -0
- chibi/services/providers/tools/exceptions.py +1 -0
- chibi/services/providers/tools/file_editor.py +875 -0
- chibi/services/providers/tools/mcp_management.py +274 -0
- chibi/services/providers/tools/mcp_simple.py +72 -0
- chibi/services/providers/tools/media.py +451 -0
- chibi/services/providers/tools/memory.py +252 -0
- chibi/services/providers/tools/schemas.py +10 -0
- chibi/services/providers/tools/send.py +435 -0
- chibi/services/providers/tools/tool.py +163 -0
- chibi/services/providers/tools/utils.py +146 -0
- chibi/services/providers/tools/web.py +261 -0
- chibi/services/providers/utils.py +182 -0
- chibi/services/task_manager.py +93 -0
- chibi/services/user.py +269 -0
- chibi/storage/abstract.py +54 -0
- chibi/storage/database.py +86 -0
- chibi/storage/dynamodb.py +257 -0
- chibi/storage/local.py +70 -0
- chibi/storage/redis.py +91 -0
- chibi/utils/__init__.py +0 -0
- chibi/utils/app.py +249 -0
- chibi/utils/telegram.py +521 -0
- chibi_bot-1.6.0b0.dist-info/LICENSE +21 -0
- chibi_bot-1.6.0b0.dist-info/METADATA +340 -0
- chibi_bot-1.6.0b0.dist-info/RECORD +70 -0
- chibi_bot-1.6.0b0.dist-info/WHEEL +4 -0
- chibi_bot-1.6.0b0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import random
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from asyncio import sleep
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from typing import Any, Awaitable, Callable, Generic, Literal, Optional, ParamSpec, TypeVar, cast
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from anthropic import AsyncClient, NotGiven, Omit
|
|
13
|
+
from anthropic.types import (
|
|
14
|
+
CacheControlEphemeralParam,
|
|
15
|
+
MessageParam,
|
|
16
|
+
TextBlock,
|
|
17
|
+
TextBlockParam,
|
|
18
|
+
ToolParam,
|
|
19
|
+
ToolResultBlockParam,
|
|
20
|
+
ToolUseBlock,
|
|
21
|
+
)
|
|
22
|
+
from anthropic.types import (
|
|
23
|
+
Message as AnthropicMessage,
|
|
24
|
+
)
|
|
25
|
+
from httpx import Response
|
|
26
|
+
from httpx._types import QueryParamTypes, RequestData
|
|
27
|
+
from loguru import logger
|
|
28
|
+
from openai import (
|
|
29
|
+
NOT_GIVEN,
|
|
30
|
+
APIConnectionError,
|
|
31
|
+
AsyncOpenAI,
|
|
32
|
+
AuthenticationError,
|
|
33
|
+
OpenAIError,
|
|
34
|
+
RateLimitError,
|
|
35
|
+
)
|
|
36
|
+
from openai import (
|
|
37
|
+
NotGiven as OpenAINotGiven,
|
|
38
|
+
)
|
|
39
|
+
from openai.types import ImagesResponse
|
|
40
|
+
from openai.types.chat import (
|
|
41
|
+
ChatCompletionAssistantMessageParam,
|
|
42
|
+
ChatCompletionMessageParam,
|
|
43
|
+
ChatCompletionMessageToolCall,
|
|
44
|
+
ChatCompletionMessageToolCallParam,
|
|
45
|
+
ChatCompletionSystemMessageParam,
|
|
46
|
+
ChatCompletionToolMessageParam,
|
|
47
|
+
)
|
|
48
|
+
from openai.types.chat.chat_completion import ChatCompletion, Choice
|
|
49
|
+
from openai.types.chat.chat_completion_message_tool_call_param import Function
|
|
50
|
+
from telegram import Update
|
|
51
|
+
from telegram.ext import ContextTypes
|
|
52
|
+
|
|
53
|
+
from chibi.config import application_settings, gpt_settings
|
|
54
|
+
from chibi.constants import IMAGE_SIZE_LITERAL
|
|
55
|
+
from chibi.exceptions import (
|
|
56
|
+
NoApiKeyProvidedError,
|
|
57
|
+
NoModelSelectedError,
|
|
58
|
+
NoResponseError,
|
|
59
|
+
NotAuthorizedError,
|
|
60
|
+
ServiceConnectionError,
|
|
61
|
+
ServiceRateLimitError,
|
|
62
|
+
ServiceResponseError,
|
|
63
|
+
)
|
|
64
|
+
from chibi.models import Message, User
|
|
65
|
+
from chibi.schemas.app import ChatResponseSchema, ModelChangeSchema, ModeratorsAnswer
|
|
66
|
+
from chibi.services.metrics import MetricsService
|
|
67
|
+
from chibi.services.providers.tools import RegisteredChibiTools
|
|
68
|
+
from chibi.services.providers.tools.constants import MODERATOR_PROMPT
|
|
69
|
+
from chibi.services.providers.utils import (
|
|
70
|
+
get_usage_from_anthropic_response,
|
|
71
|
+
get_usage_from_openai_response,
|
|
72
|
+
get_usage_msg,
|
|
73
|
+
prepare_system_prompt,
|
|
74
|
+
send_llm_thoughts,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
P = ParamSpec("P")
|
|
78
|
+
R = TypeVar("R")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RegisteredProviders:
|
|
82
|
+
all: dict[str, type["Provider"]] = {}
|
|
83
|
+
available: dict[str, type["Provider"]] = {}
|
|
84
|
+
|
|
85
|
+
def __init__(self, user_api_keys: dict[str, str] | None = None) -> None:
|
|
86
|
+
self.tokens = {} if not user_api_keys else user_api_keys
|
|
87
|
+
if gpt_settings.public_mode:
|
|
88
|
+
self.available: dict[str, type["Provider"]] = {
|
|
89
|
+
provider.name: provider for provider in RegisteredProviders.all.values() if provider.name in self.tokens
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def register(cls, provider: type["Provider"]) -> None:
|
|
94
|
+
cls.all[provider.name] = provider
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def register_as_available(cls, provider: type["Provider"]) -> None:
|
|
98
|
+
cls.available[provider.name] = provider
|
|
99
|
+
|
|
100
|
+
def get_api_key(self, provider: type["Provider"]) -> str | None:
|
|
101
|
+
if not gpt_settings.public_mode:
|
|
102
|
+
return provider.api_key
|
|
103
|
+
|
|
104
|
+
if provider.name not in self.tokens:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
return self.tokens[provider.name]
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def available_instances(self) -> list["Provider"]:
|
|
111
|
+
return [
|
|
112
|
+
provider(token=self.get_api_key(provider)) # type: ignore
|
|
113
|
+
for provider in self.available.values()
|
|
114
|
+
if self.get_api_key(provider) is not None
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def chat_ready(self) -> dict[str, type["Provider"]]:
|
|
119
|
+
return {provider_name: provider for provider_name, provider in self.available.items() if provider.chat_ready}
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def moderation_ready(self) -> dict[str, type["Provider"]]:
|
|
123
|
+
return {
|
|
124
|
+
provider_name: provider for provider_name, provider in self.available.items() if provider.moderation_ready
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def image_generation_ready(self) -> dict[str, type["Provider"]]:
|
|
129
|
+
return {name: provider for name, provider in self.available.items() if provider.image_generation_ready}
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def stt_ready(self) -> dict[str, type["Provider"]]:
|
|
133
|
+
return {name: provider for name, provider in self.available.items() if provider.stt_ready}
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def tts_ready(self) -> dict[str, type["Provider"]]:
|
|
137
|
+
return {name: provider for name, provider in self.available.items() if provider.tts_ready}
|
|
138
|
+
|
|
139
|
+
def get_instance(self, provider: type["Provider"]) -> Optional["Provider"]:
|
|
140
|
+
api_key = self.get_api_key(provider)
|
|
141
|
+
if not api_key:
|
|
142
|
+
return None
|
|
143
|
+
return provider(token=api_key)
|
|
144
|
+
|
|
145
|
+
def get(self, provider_name: str) -> Optional["Provider"]:
|
|
146
|
+
if provider_name not in self.available:
|
|
147
|
+
return None
|
|
148
|
+
provider = self.available[provider_name]
|
|
149
|
+
return self.get_instance(provider=provider)
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def get_class(cls, provider_name: str) -> Optional[type["Provider"]]:
|
|
153
|
+
return cls.all.get(provider_name)
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def first_tts_ready(self) -> Optional["Provider"]:
|
|
157
|
+
if provider := next(iter(self.tts_ready.values()), None):
|
|
158
|
+
return self.get_instance(provider=provider)
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def first_stt_ready(self) -> Optional["Provider"]:
|
|
163
|
+
if provider := next(iter(self.stt_ready.values()), None):
|
|
164
|
+
return self.get_instance(provider=provider)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def first_image_generation_ready(self) -> Optional["Provider"]:
|
|
169
|
+
if provider := next(iter(self.image_generation_ready.values()), None):
|
|
170
|
+
return self.get_instance(provider=provider)
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def first_chat_ready(self) -> Optional["Provider"]:
|
|
175
|
+
if provider := next(iter(self.chat_ready.values()), None):
|
|
176
|
+
return self.get_instance(provider=provider)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def first_moderation_ready(self) -> Optional["Provider"]:
|
|
181
|
+
if provider := next(reversed(self.moderation_ready.values()), None):
|
|
182
|
+
return self.get_instance(provider=provider)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Provider(ABC):
|
|
187
|
+
api_key: str | None = None
|
|
188
|
+
stt_ready: bool = False
|
|
189
|
+
tts_ready: bool = False
|
|
190
|
+
ocr_ready: bool = False
|
|
191
|
+
chat_ready: bool = False
|
|
192
|
+
moderation_ready: bool = False
|
|
193
|
+
image_generation_ready: bool = False
|
|
194
|
+
|
|
195
|
+
name: str
|
|
196
|
+
model_name_keywords: list[str] = []
|
|
197
|
+
model_name_prefixes: list[str] = []
|
|
198
|
+
model_name_keywords_exclude: list[str] = []
|
|
199
|
+
default_model: str
|
|
200
|
+
default_image_model: str | None = None
|
|
201
|
+
default_stt_model: str | None = None
|
|
202
|
+
default_tts_voice: str | None = None
|
|
203
|
+
default_tts_model: str | None = None
|
|
204
|
+
default_moderation_model: str | None = None
|
|
205
|
+
timeout: int = gpt_settings.timeout
|
|
206
|
+
|
|
207
|
+
def __init__(self, token: str) -> None:
|
|
208
|
+
self.token = token
|
|
209
|
+
|
|
210
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
211
|
+
super().__init_subclass__(**kwargs)
|
|
212
|
+
|
|
213
|
+
if hasattr(cls, "name"):
|
|
214
|
+
RegisteredProviders.register(cls)
|
|
215
|
+
|
|
216
|
+
if cls.api_key:
|
|
217
|
+
RegisteredProviders.register_as_available(cls)
|
|
218
|
+
|
|
219
|
+
async def get_chat_response(
|
|
220
|
+
self,
|
|
221
|
+
messages: list[Message],
|
|
222
|
+
user: User | None = None,
|
|
223
|
+
model: str | None = None,
|
|
224
|
+
system_prompt: str = gpt_settings.assistant_prompt,
|
|
225
|
+
update: Update | None = None,
|
|
226
|
+
context: ContextTypes.DEFAULT_TYPE | None = None,
|
|
227
|
+
) -> tuple[ChatResponseSchema, list[Message]]:
|
|
228
|
+
raise NotImplementedError
|
|
229
|
+
|
|
230
|
+
async def get_available_models(self, image_generation: bool = False) -> list[ModelChangeSchema]:
|
|
231
|
+
raise NotImplementedError
|
|
232
|
+
|
|
233
|
+
def get_model_display_name(self, model_name: str) -> str:
|
|
234
|
+
raise NotImplementedError
|
|
235
|
+
|
|
236
|
+
async def transcribe(self, audio: BytesIO, model: str | None = None) -> str:
|
|
237
|
+
raise NotImplementedError
|
|
238
|
+
|
|
239
|
+
async def speech(self, text: str, voice: str | None = None, model: str | None = None) -> bytes:
|
|
240
|
+
raise NotImplementedError
|
|
241
|
+
|
|
242
|
+
async def moderate_command(self, cmd: str, model: str | None = None) -> ModeratorsAnswer:
|
|
243
|
+
raise NotImplementedError
|
|
244
|
+
|
|
245
|
+
async def api_key_is_valid(self) -> bool:
|
|
246
|
+
try:
|
|
247
|
+
await self.get_available_models()
|
|
248
|
+
except Exception: # Some providers return 403, others - 400... Okay..
|
|
249
|
+
return False
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def _model_name_has_prefix(cls, model_name: str) -> bool:
|
|
254
|
+
if not cls.model_name_prefixes:
|
|
255
|
+
return True
|
|
256
|
+
for prefix in cls.model_name_prefixes:
|
|
257
|
+
if model_name.startswith(prefix):
|
|
258
|
+
return True
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
def _model_name_has_keyword(cls, model_name: str) -> bool:
|
|
263
|
+
if not cls.model_name_keywords:
|
|
264
|
+
return True
|
|
265
|
+
for keyword in cls.model_name_keywords:
|
|
266
|
+
if keyword in model_name:
|
|
267
|
+
return True
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
def _model_name_has_keywords_exclude(cls, model_name: str) -> bool:
|
|
272
|
+
if not cls.model_name_keywords_exclude:
|
|
273
|
+
return False
|
|
274
|
+
for keyword in cls.model_name_keywords_exclude:
|
|
275
|
+
if keyword in model_name:
|
|
276
|
+
return True
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def is_chat_ready_model(cls, model_name: str) -> bool:
|
|
281
|
+
return all(
|
|
282
|
+
(
|
|
283
|
+
cls._model_name_has_prefix(model_name),
|
|
284
|
+
cls._model_name_has_keyword(model_name),
|
|
285
|
+
not cls._model_name_has_keywords_exclude(model_name),
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def is_image_ready_model(cls, model_name: str) -> bool:
|
|
291
|
+
return "image" in model_name
|
|
292
|
+
|
|
293
|
+
async def get_images(self, prompt: str, model: str | None) -> list[str] | list[BytesIO]:
|
|
294
|
+
raise NotImplementedError
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class OpenAIFriendlyProvider(Provider, Generic[P, R]):
|
|
298
|
+
temperature: float | OpenAINotGiven | None = gpt_settings.temperature
|
|
299
|
+
max_tokens: int | OpenAINotGiven | None = gpt_settings.max_tokens
|
|
300
|
+
presence_penalty: float | OpenAINotGiven | None = gpt_settings.presence_penalty
|
|
301
|
+
frequency_penalty: float | OpenAINotGiven | None = gpt_settings.frequency_penalty
|
|
302
|
+
image_quality: Literal["standard", "hd"] | OpenAINotGiven = gpt_settings.image_quality
|
|
303
|
+
image_size: IMAGE_SIZE_LITERAL | OpenAINotGiven | None = gpt_settings.image_size
|
|
304
|
+
base_url: str
|
|
305
|
+
image_n_choices: int = gpt_settings.image_n_choices
|
|
306
|
+
|
|
307
|
+
def __getattribute__(self, name: str) -> object:
|
|
308
|
+
attr = super().__getattribute__(name)
|
|
309
|
+
|
|
310
|
+
if callable(attr):
|
|
311
|
+
if inspect.iscoroutinefunction(attr):
|
|
312
|
+
attr_async_callable = cast(Callable[P, Awaitable[R]], attr)
|
|
313
|
+
|
|
314
|
+
@wraps(attr_async_callable)
|
|
315
|
+
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
|
|
316
|
+
model_name = cast(str, kwargs.get("model", "unknown"))
|
|
317
|
+
try:
|
|
318
|
+
return await attr_async_callable(*args, **kwargs)
|
|
319
|
+
except APIConnectionError:
|
|
320
|
+
raise ServiceConnectionError(provider=self.name, model=model_name)
|
|
321
|
+
except AuthenticationError:
|
|
322
|
+
raise NotAuthorizedError(provider=self.name, model=model_name)
|
|
323
|
+
except RateLimitError:
|
|
324
|
+
raise ServiceRateLimitError(provider=self.name, model=model_name)
|
|
325
|
+
except OpenAIError as e:
|
|
326
|
+
logger.error(e)
|
|
327
|
+
raise ServiceResponseError(provider=self.name, model=model_name)
|
|
328
|
+
|
|
329
|
+
return async_wrapper
|
|
330
|
+
else:
|
|
331
|
+
attr_callable = cast(Callable[P, R], attr)
|
|
332
|
+
|
|
333
|
+
@wraps(attr_callable)
|
|
334
|
+
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
|
|
335
|
+
return attr_callable(*args, **kwargs)
|
|
336
|
+
|
|
337
|
+
return sync_wrapper
|
|
338
|
+
|
|
339
|
+
return attr
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def client(self) -> AsyncOpenAI:
|
|
343
|
+
if not self.token:
|
|
344
|
+
raise NoApiKeyProvidedError(provider=self.name)
|
|
345
|
+
return AsyncOpenAI(api_key=self.token, base_url=self.base_url)
|
|
346
|
+
|
|
347
|
+
async def get_chat_response(
|
|
348
|
+
self,
|
|
349
|
+
messages: list[Message],
|
|
350
|
+
user: User | None = None,
|
|
351
|
+
model: str | None = None,
|
|
352
|
+
system_prompt: str = gpt_settings.assistant_prompt,
|
|
353
|
+
update: Update | None = None,
|
|
354
|
+
context: ContextTypes.DEFAULT_TYPE | None = None,
|
|
355
|
+
) -> tuple[ChatResponseSchema, list[Message]]:
|
|
356
|
+
model = model or self.default_model
|
|
357
|
+
|
|
358
|
+
initial_messages = [msg.to_openai() for msg in messages]
|
|
359
|
+
chat_response, updated_messages = await self._get_chat_completion_response(
|
|
360
|
+
messages=initial_messages.copy(),
|
|
361
|
+
model=model,
|
|
362
|
+
system_prompt=system_prompt,
|
|
363
|
+
user=user,
|
|
364
|
+
context=context,
|
|
365
|
+
update=update,
|
|
366
|
+
)
|
|
367
|
+
new_messages = [msg for msg in updated_messages if msg not in initial_messages]
|
|
368
|
+
return (
|
|
369
|
+
chat_response,
|
|
370
|
+
[Message.from_openai(msg) for msg in new_messages],
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
async def _get_chat_completion_response(
|
|
374
|
+
self,
|
|
375
|
+
messages: list[ChatCompletionMessageParam],
|
|
376
|
+
model: str,
|
|
377
|
+
user: User | None = None,
|
|
378
|
+
system_prompt: str | None = None,
|
|
379
|
+
update: Update | None = None,
|
|
380
|
+
context: ContextTypes.DEFAULT_TYPE | None = None,
|
|
381
|
+
) -> tuple[ChatResponseSchema, list[ChatCompletionMessageParam]]:
|
|
382
|
+
if not system_prompt:
|
|
383
|
+
dialog = messages
|
|
384
|
+
else:
|
|
385
|
+
prepared_system_prompt = await prepare_system_prompt(base_system_prompt=system_prompt, user=user)
|
|
386
|
+
system_message = ChatCompletionSystemMessageParam(role="system", content=prepared_system_prompt)
|
|
387
|
+
dialog: list[ChatCompletionMessageParam] = [system_message] + messages # type: ignore
|
|
388
|
+
|
|
389
|
+
temperature = 1 if model.startswith("o") else self.temperature
|
|
390
|
+
|
|
391
|
+
response: ChatCompletion = await self.client.chat.completions.create(
|
|
392
|
+
model=model,
|
|
393
|
+
messages=dialog,
|
|
394
|
+
temperature=temperature,
|
|
395
|
+
max_tokens=self.max_tokens,
|
|
396
|
+
presence_penalty=self.presence_penalty,
|
|
397
|
+
frequency_penalty=self.frequency_penalty,
|
|
398
|
+
timeout=self.timeout,
|
|
399
|
+
tools=RegisteredChibiTools.get_tool_definitions(),
|
|
400
|
+
tool_choice="auto",
|
|
401
|
+
reasoning_effort="medium" if "reason" in model else NOT_GIVEN,
|
|
402
|
+
)
|
|
403
|
+
choices: list[Choice] = response.choices
|
|
404
|
+
|
|
405
|
+
if len(choices) == 0:
|
|
406
|
+
raise ServiceResponseError(provider=self.name, model=model, detail="Unexpected (empty) response received")
|
|
407
|
+
|
|
408
|
+
data = choices[0]
|
|
409
|
+
answer: str = data.message.content or ""
|
|
410
|
+
|
|
411
|
+
usage = get_usage_from_openai_response(response_message=response)
|
|
412
|
+
if application_settings.is_influx_configured:
|
|
413
|
+
MetricsService.send_usage_metrics(metric=usage, model=model, provider=self.name, user=user)
|
|
414
|
+
usage_message = get_usage_msg(usage=usage)
|
|
415
|
+
|
|
416
|
+
tool_calls: list[ChatCompletionMessageToolCall] | None = data.message.tool_calls
|
|
417
|
+
|
|
418
|
+
if not tool_calls:
|
|
419
|
+
messages.append(ChatCompletionAssistantMessageParam(**data.message.model_dump())) # type: ignore
|
|
420
|
+
return ChatResponseSchema(answer=answer, provider=self.name, model=model, usage=usage), messages
|
|
421
|
+
|
|
422
|
+
# Tool calls handling
|
|
423
|
+
logger.log("CALL", f"{model} requested the call of {len(tool_calls)} tools.")
|
|
424
|
+
|
|
425
|
+
thoughts = answer or "No thoughts"
|
|
426
|
+
if answer:
|
|
427
|
+
await send_llm_thoughts(thoughts=thoughts, context=context, update=update)
|
|
428
|
+
logger.log("THINK", f"{model}: {thoughts}. {usage_message}")
|
|
429
|
+
|
|
430
|
+
tool_context: dict[str, Any] = {
|
|
431
|
+
"user_id": user.id if user else None,
|
|
432
|
+
"telegram_context": context,
|
|
433
|
+
"telegram_update": update,
|
|
434
|
+
"model": model,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
tool_coroutines = [
|
|
438
|
+
RegisteredChibiTools.call(
|
|
439
|
+
tool_name=tool_call.function.name, tools_args=tool_context | json.loads(tool_call.function.arguments)
|
|
440
|
+
)
|
|
441
|
+
for tool_call in tool_calls
|
|
442
|
+
]
|
|
443
|
+
results = await asyncio.gather(*tool_coroutines)
|
|
444
|
+
|
|
445
|
+
for tool_call, result in zip(tool_calls, results):
|
|
446
|
+
tool_call_message = ChatCompletionAssistantMessageParam(
|
|
447
|
+
role="assistant",
|
|
448
|
+
content=answer,
|
|
449
|
+
tool_calls=[
|
|
450
|
+
ChatCompletionMessageToolCallParam(
|
|
451
|
+
id=tool_call.id,
|
|
452
|
+
type="function",
|
|
453
|
+
function=Function(
|
|
454
|
+
name=tool_call.function.name,
|
|
455
|
+
arguments=tool_call.function.arguments,
|
|
456
|
+
),
|
|
457
|
+
)
|
|
458
|
+
],
|
|
459
|
+
)
|
|
460
|
+
tool_result_message = ChatCompletionToolMessageParam(
|
|
461
|
+
tool_call_id=tool_call.id,
|
|
462
|
+
role="tool",
|
|
463
|
+
content=result.model_dump_json(),
|
|
464
|
+
)
|
|
465
|
+
messages.append(tool_call_message)
|
|
466
|
+
messages.append(tool_result_message)
|
|
467
|
+
|
|
468
|
+
logger.log("CALL", "All the function results have been obtained. Returning them to the LLM...")
|
|
469
|
+
return await self._get_chat_completion_response(
|
|
470
|
+
messages=messages,
|
|
471
|
+
model=model,
|
|
472
|
+
user=user,
|
|
473
|
+
system_prompt=system_prompt,
|
|
474
|
+
context=context,
|
|
475
|
+
update=update,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
async def moderate_command(self, cmd: str, model: str | None = None) -> ModeratorsAnswer:
|
|
479
|
+
moderator_model = model or self.default_moderation_model or self.default_model
|
|
480
|
+
system_message = ChatCompletionSystemMessageParam(role="system", content=MODERATOR_PROMPT)
|
|
481
|
+
|
|
482
|
+
messages = [
|
|
483
|
+
Message(role="user", content=cmd).to_openai(),
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
dialog: list[ChatCompletionMessageParam] = [system_message] + messages
|
|
487
|
+
temperature = (
|
|
488
|
+
1 if moderator_model.startswith("o") or "mini" in moderator_model or "nano" in moderator_model else 0.0
|
|
489
|
+
)
|
|
490
|
+
response: ChatCompletion = await self.client.chat.completions.create(
|
|
491
|
+
model=moderator_model,
|
|
492
|
+
messages=dialog,
|
|
493
|
+
temperature=temperature,
|
|
494
|
+
max_completion_tokens=1024,
|
|
495
|
+
presence_penalty=self.presence_penalty,
|
|
496
|
+
frequency_penalty=self.frequency_penalty,
|
|
497
|
+
timeout=self.timeout,
|
|
498
|
+
# reasoning_effort="medium" if "reason" in moderator_model else NOT_GIVEN,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
choices: list[Choice] = response.choices
|
|
502
|
+
|
|
503
|
+
if len(choices) == 0:
|
|
504
|
+
raise ServiceResponseError(
|
|
505
|
+
provider=self.name, model=moderator_model, detail="Unexpected (empty) response received"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
data = choices[0]
|
|
509
|
+
answer: str = data.message.content or ""
|
|
510
|
+
|
|
511
|
+
usage = get_usage_from_openai_response(response_message=response)
|
|
512
|
+
if application_settings.is_influx_configured:
|
|
513
|
+
MetricsService.send_usage_metrics(metric=usage, model=moderator_model, provider=self.name)
|
|
514
|
+
# usage_message = get_usage_msg(usage=usage)
|
|
515
|
+
answer = answer.strip("```")
|
|
516
|
+
answer = answer.strip("json")
|
|
517
|
+
answer = answer.strip()
|
|
518
|
+
try:
|
|
519
|
+
result_data = json.loads(answer)
|
|
520
|
+
except Exception:
|
|
521
|
+
logger.error(f"Error parsing moderator's response: {answer}")
|
|
522
|
+
return ModeratorsAnswer(verdict="declined", reason=answer, status="error")
|
|
523
|
+
|
|
524
|
+
verdict = result_data.get("verdict", "declined")
|
|
525
|
+
if verdict == "accepted":
|
|
526
|
+
return ModeratorsAnswer(verdict="accepted", status="ok")
|
|
527
|
+
|
|
528
|
+
reason = result_data.get("reason", None)
|
|
529
|
+
if reason is None:
|
|
530
|
+
logger.error(f"Moderator did not provide reason properly: {answer}")
|
|
531
|
+
|
|
532
|
+
return ModeratorsAnswer(verdict="declined", reason=reason, status="operation aborted")
|
|
533
|
+
|
|
534
|
+
def get_model_display_name(self, model_name: str) -> str:
|
|
535
|
+
return model_name.replace("-", " ")
|
|
536
|
+
|
|
537
|
+
async def get_available_models(self, image_generation: bool = False) -> list[ModelChangeSchema]:
|
|
538
|
+
try:
|
|
539
|
+
models = await self.client.models.list()
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.error(f"Failed to get available models for provider {self.name} due to exception: {e}")
|
|
542
|
+
return []
|
|
543
|
+
|
|
544
|
+
all_models = [
|
|
545
|
+
ModelChangeSchema(
|
|
546
|
+
provider=self.name,
|
|
547
|
+
name=model.id,
|
|
548
|
+
display_name=self.get_model_display_name(model.id),
|
|
549
|
+
image_generation=self.is_image_ready_model(model.id),
|
|
550
|
+
)
|
|
551
|
+
for model in models.data
|
|
552
|
+
]
|
|
553
|
+
all_models.sort(key=lambda model: model.name)
|
|
554
|
+
|
|
555
|
+
if image_generation:
|
|
556
|
+
return [model for model in all_models if model.image_generation]
|
|
557
|
+
|
|
558
|
+
if gpt_settings.models_whitelist:
|
|
559
|
+
return [model for model in all_models if model.name in gpt_settings.models_whitelist]
|
|
560
|
+
|
|
561
|
+
return [model for model in all_models if self.is_chat_ready_model(model.name)]
|
|
562
|
+
|
|
563
|
+
async def _get_image_generation_response(self, prompt: str, model: str) -> ImagesResponse:
|
|
564
|
+
return await self.client.images.generate(
|
|
565
|
+
model=model,
|
|
566
|
+
prompt=prompt,
|
|
567
|
+
n=gpt_settings.image_n_choices,
|
|
568
|
+
quality=self.image_quality,
|
|
569
|
+
size=self.image_size,
|
|
570
|
+
timeout=gpt_settings.timeout,
|
|
571
|
+
response_format="url",
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
async def get_images(self, prompt: str, model: str | None = None) -> list[str] | list[BytesIO]:
|
|
575
|
+
model = model or self.default_image_model
|
|
576
|
+
if not model:
|
|
577
|
+
raise NoModelSelectedError(provider=self.name, detail="No image generation model selected")
|
|
578
|
+
response = await self._get_image_generation_response(prompt=prompt, model=model)
|
|
579
|
+
if not response.data:
|
|
580
|
+
raise ServiceResponseError(provider=self.name, model=model, detail="No image data received.")
|
|
581
|
+
return [image.url for image in response.data if image.url]
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
class RestApiFriendlyProvider(Provider):
|
|
585
|
+
@property
|
|
586
|
+
def _headers(self) -> dict[str, str]:
|
|
587
|
+
raise NotImplementedError
|
|
588
|
+
|
|
589
|
+
def get_async_httpx_client(self) -> httpx.AsyncClient:
|
|
590
|
+
transport = httpx.AsyncHTTPTransport(retries=gpt_settings.retries, proxy=gpt_settings.proxy)
|
|
591
|
+
return httpx.AsyncClient(transport=transport, timeout=gpt_settings.timeout)
|
|
592
|
+
|
|
593
|
+
async def _request(
|
|
594
|
+
self,
|
|
595
|
+
method: str,
|
|
596
|
+
url: str,
|
|
597
|
+
data: RequestData | None = None,
|
|
598
|
+
params: QueryParamTypes | None = None,
|
|
599
|
+
headers: dict[str, str] | None = None,
|
|
600
|
+
) -> Response:
|
|
601
|
+
if not self.token:
|
|
602
|
+
raise NoApiKeyProvidedError(provider=self.name)
|
|
603
|
+
|
|
604
|
+
try:
|
|
605
|
+
async with self.get_async_httpx_client() as client:
|
|
606
|
+
response = await client.request(
|
|
607
|
+
method=method,
|
|
608
|
+
url=url,
|
|
609
|
+
json=data,
|
|
610
|
+
headers=headers or self._headers,
|
|
611
|
+
params=params,
|
|
612
|
+
)
|
|
613
|
+
except Exception as e:
|
|
614
|
+
logger.error(f"An error occurred while calling the {self.name} API: {e}")
|
|
615
|
+
raise ServiceResponseError(provider=self.name, detail=str(e))
|
|
616
|
+
|
|
617
|
+
if response.status_code == 200:
|
|
618
|
+
return response
|
|
619
|
+
|
|
620
|
+
logger.error(
|
|
621
|
+
f"Unexpected response from {self.name} API. Status code: {response.status_code}. Data: {response.text}"
|
|
622
|
+
)
|
|
623
|
+
if response.status_code == 401:
|
|
624
|
+
raise NotAuthorizedError(provider=self.name)
|
|
625
|
+
if response.status_code == 429:
|
|
626
|
+
raise ServiceRateLimitError(provider=self.name)
|
|
627
|
+
raise ServiceResponseError(provider=self.name)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class AnthropicFriendlyProvider(RestApiFriendlyProvider):
|
|
631
|
+
frequency_penalty: float | NotGiven | None = gpt_settings.frequency_penalty
|
|
632
|
+
max_tokens: int = gpt_settings.max_tokens
|
|
633
|
+
presence_penalty: float | NotGiven = gpt_settings.presence_penalty
|
|
634
|
+
temperature: float | Omit = gpt_settings.temperature
|
|
635
|
+
|
|
636
|
+
@property
|
|
637
|
+
def tools_list(self) -> list[ToolParam]:
|
|
638
|
+
anthropic_tools = [
|
|
639
|
+
ToolParam(
|
|
640
|
+
name=tool["function"]["name"],
|
|
641
|
+
description=tool["function"]["description"],
|
|
642
|
+
input_schema=tool["function"]["parameters"],
|
|
643
|
+
)
|
|
644
|
+
for tool in RegisteredChibiTools.get_tool_definitions()
|
|
645
|
+
]
|
|
646
|
+
return anthropic_tools
|
|
647
|
+
|
|
648
|
+
@property
|
|
649
|
+
def client(self) -> AsyncClient:
|
|
650
|
+
raise NotImplementedError
|
|
651
|
+
|
|
652
|
+
async def _generate_content(
|
|
653
|
+
self,
|
|
654
|
+
model: str,
|
|
655
|
+
system_prompt: str,
|
|
656
|
+
messages: list[MessageParam],
|
|
657
|
+
) -> AnthropicMessage:
|
|
658
|
+
for attempt in range(gpt_settings.retries):
|
|
659
|
+
response_message: AnthropicMessage = await self.client.messages.create(
|
|
660
|
+
model=model,
|
|
661
|
+
max_tokens=self.max_tokens,
|
|
662
|
+
temperature=self.temperature,
|
|
663
|
+
timeout=self.timeout,
|
|
664
|
+
tools=self.tools_list,
|
|
665
|
+
system=[
|
|
666
|
+
TextBlockParam(
|
|
667
|
+
text=system_prompt,
|
|
668
|
+
type="text",
|
|
669
|
+
cache_control=CacheControlEphemeralParam(type="ephemeral"),
|
|
670
|
+
)
|
|
671
|
+
],
|
|
672
|
+
messages=messages,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
if response_message.content and len(response_message.content) > 0:
|
|
676
|
+
return response_message
|
|
677
|
+
|
|
678
|
+
delay = gpt_settings.backoff_factor * (2**attempt)
|
|
679
|
+
jitter = delay * random.uniform(0.1, 0.5)
|
|
680
|
+
total_delay = delay + jitter
|
|
681
|
+
|
|
682
|
+
logger.warning(
|
|
683
|
+
f"Attempt #{attempt + 1}. Unexpected (empty) response received. Retrying in {total_delay} seconds..."
|
|
684
|
+
)
|
|
685
|
+
await sleep(total_delay)
|
|
686
|
+
raise NoResponseError(provider=self.name, model=model, detail="Unexpected (empty) response received")
|
|
687
|
+
|
|
688
|
+
async def get_chat_response(
|
|
689
|
+
self,
|
|
690
|
+
messages: list[Message],
|
|
691
|
+
user: User | None = None,
|
|
692
|
+
model: str | None = None,
|
|
693
|
+
system_prompt: str = gpt_settings.assistant_prompt,
|
|
694
|
+
update: Update | None = None,
|
|
695
|
+
context: ContextTypes.DEFAULT_TYPE | None = None,
|
|
696
|
+
) -> tuple[ChatResponseSchema, list[Message]]:
|
|
697
|
+
model = model or self.default_model
|
|
698
|
+
initial_messages = [msg.to_anthropic() for msg in messages]
|
|
699
|
+
|
|
700
|
+
if len(initial_messages) >= 2:
|
|
701
|
+
initial_messages[-2]["content"][0]["cache_control"] = {"type": "ephemeral"} # type: ignore
|
|
702
|
+
|
|
703
|
+
chat_response, updated_messages = await self._get_chat_completion_response(
|
|
704
|
+
messages=initial_messages.copy(),
|
|
705
|
+
user=user,
|
|
706
|
+
model=model,
|
|
707
|
+
system_prompt=system_prompt,
|
|
708
|
+
context=context,
|
|
709
|
+
update=update,
|
|
710
|
+
)
|
|
711
|
+
new_messages = [msg for msg in updated_messages if msg not in initial_messages]
|
|
712
|
+
return (
|
|
713
|
+
chat_response,
|
|
714
|
+
[Message.from_anthropic(msg) for msg in new_messages],
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
async def _get_chat_completion_response(
|
|
718
|
+
self,
|
|
719
|
+
messages: list[MessageParam],
|
|
720
|
+
model: str,
|
|
721
|
+
user: User | None = None,
|
|
722
|
+
system_prompt: str = gpt_settings.assistant_prompt,
|
|
723
|
+
update: Update | None = None,
|
|
724
|
+
context: ContextTypes.DEFAULT_TYPE | None = None,
|
|
725
|
+
) -> tuple[ChatResponseSchema, list[MessageParam]]:
|
|
726
|
+
prepared_system_prompt = await prepare_system_prompt(base_system_prompt=system_prompt, user=user)
|
|
727
|
+
response_message: AnthropicMessage = await self._generate_content(
|
|
728
|
+
model=model,
|
|
729
|
+
system_prompt=prepared_system_prompt,
|
|
730
|
+
messages=messages,
|
|
731
|
+
)
|
|
732
|
+
usage = get_usage_from_anthropic_response(response_message=response_message)
|
|
733
|
+
|
|
734
|
+
if application_settings.is_influx_configured:
|
|
735
|
+
MetricsService.send_usage_metrics(metric=usage, user=user, model=model, provider=self.name)
|
|
736
|
+
|
|
737
|
+
tool_call_parts = [part for part in response_message.content if isinstance(part, ToolUseBlock)]
|
|
738
|
+
if not tool_call_parts:
|
|
739
|
+
messages.append(
|
|
740
|
+
MessageParam(
|
|
741
|
+
role="assistant",
|
|
742
|
+
content=[content.model_dump() for content in response_message.content], # type: ignore
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
answer = None
|
|
746
|
+
for block in response_message.content:
|
|
747
|
+
if answer := getattr(block, "text", None):
|
|
748
|
+
break
|
|
749
|
+
|
|
750
|
+
return ChatResponseSchema(
|
|
751
|
+
answer=answer or "no data",
|
|
752
|
+
provider=self.name,
|
|
753
|
+
model=model,
|
|
754
|
+
usage=usage,
|
|
755
|
+
), messages
|
|
756
|
+
|
|
757
|
+
# Tool calls handling
|
|
758
|
+
logger.log("CALL", f"{model} requested the call of {len(tool_call_parts)} tools.")
|
|
759
|
+
thoughts_part: TextBlock | None = next(
|
|
760
|
+
(part for part in response_message.content if isinstance(part, TextBlock)), None
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
if thoughts_part:
|
|
764
|
+
await send_llm_thoughts(thoughts=thoughts_part.text, context=context, update=update)
|
|
765
|
+
|
|
766
|
+
logger.log(
|
|
767
|
+
"THINK", f"{model}: {thoughts_part.text if thoughts_part else 'No thoughts'}. {get_usage_msg(usage=usage)}"
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
tool_context: dict[str, Any] = {
|
|
771
|
+
"user_id": user.id if user else None,
|
|
772
|
+
"telegram_context": context,
|
|
773
|
+
"telegram_update": update,
|
|
774
|
+
"model": model,
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
tool_coroutines = [
|
|
778
|
+
RegisteredChibiTools.call(tool_name=tool_call_part.name, tools_args=tool_context | tool_call_part.input)
|
|
779
|
+
for tool_call_part in tool_call_parts
|
|
780
|
+
]
|
|
781
|
+
results = await asyncio.gather(*tool_coroutines)
|
|
782
|
+
|
|
783
|
+
for tool_call_part, result in zip(tool_call_parts, results):
|
|
784
|
+
tool_call_message = MessageParam(
|
|
785
|
+
role="assistant",
|
|
786
|
+
content=[part.model_dump() for part in (thoughts_part, tool_call_part) if part is not None], # type: ignore
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
tool_result_message = MessageParam(
|
|
790
|
+
role="user",
|
|
791
|
+
content=[
|
|
792
|
+
ToolResultBlockParam(
|
|
793
|
+
type="tool_result",
|
|
794
|
+
tool_use_id=tool_call_part.id,
|
|
795
|
+
content=result.model_dump_json(),
|
|
796
|
+
)
|
|
797
|
+
],
|
|
798
|
+
)
|
|
799
|
+
messages.append(tool_call_message)
|
|
800
|
+
messages.append(tool_result_message)
|
|
801
|
+
|
|
802
|
+
logger.log("CALL", "All the function results have been obtained. Returning them to the LLM...")
|
|
803
|
+
return await self._get_chat_completion_response(
|
|
804
|
+
messages=messages,
|
|
805
|
+
model=model,
|
|
806
|
+
user=user,
|
|
807
|
+
system_prompt=system_prompt,
|
|
808
|
+
context=context,
|
|
809
|
+
update=update,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
async def moderate_command(self, cmd: str, model: str | None = None) -> ModeratorsAnswer:
|
|
813
|
+
moderator_model = model or self.default_moderation_model or self.default_model
|
|
814
|
+
messages = [Message(role="user", content=cmd).to_anthropic()]
|
|
815
|
+
response_message: AnthropicMessage = await self.client.messages.create(
|
|
816
|
+
model=moderator_model,
|
|
817
|
+
max_tokens=1024,
|
|
818
|
+
temperature=0.1,
|
|
819
|
+
timeout=self.timeout,
|
|
820
|
+
system=[
|
|
821
|
+
TextBlockParam(
|
|
822
|
+
text=MODERATOR_PROMPT,
|
|
823
|
+
type="text",
|
|
824
|
+
)
|
|
825
|
+
],
|
|
826
|
+
tools=[
|
|
827
|
+
{
|
|
828
|
+
"name": "print_moderator_verdict",
|
|
829
|
+
"description": "Provide moderator's verdict",
|
|
830
|
+
"input_schema": {
|
|
831
|
+
"type": "object",
|
|
832
|
+
"properties": {
|
|
833
|
+
"verdict": {"type": "string"},
|
|
834
|
+
"status": {"type": "string", "default": "ok"},
|
|
835
|
+
"reason": {"type": "string"},
|
|
836
|
+
},
|
|
837
|
+
"required": ["verdict"],
|
|
838
|
+
},
|
|
839
|
+
}
|
|
840
|
+
],
|
|
841
|
+
tool_choice={"type": "tool", "name": "print_moderator_verdict"},
|
|
842
|
+
messages=messages,
|
|
843
|
+
)
|
|
844
|
+
if not response_message.content:
|
|
845
|
+
return ModeratorsAnswer(status="error", verdict="declined", reason="no response from moderator received")
|
|
846
|
+
usage = get_usage_from_anthropic_response(response_message=response_message)
|
|
847
|
+
|
|
848
|
+
if application_settings.is_influx_configured:
|
|
849
|
+
MetricsService.send_usage_metrics(metric=usage, model=moderator_model, provider=self.name)
|
|
850
|
+
tool_call: ToolUseBlock | None = next(
|
|
851
|
+
part for part in response_message.content if isinstance(part, ToolUseBlock)
|
|
852
|
+
)
|
|
853
|
+
if not tool_call:
|
|
854
|
+
return ModeratorsAnswer(status="error", verdict="declined", reason="no response from moderator received")
|
|
855
|
+
answer = tool_call.input
|
|
856
|
+
|
|
857
|
+
try:
|
|
858
|
+
return ModeratorsAnswer.model_validate(answer, extra="ignore")
|
|
859
|
+
except Exception as e:
|
|
860
|
+
msg = f"Error parsing moderator's response: {answer}. Error: {e}"
|
|
861
|
+
logger.error(msg)
|
|
862
|
+
return ModeratorsAnswer(verdict="declined", reason=msg, status="error")
|
|
863
|
+
|
|
864
|
+
async def get_available_models(self, image_generation: bool = False) -> list[ModelChangeSchema]:
|
|
865
|
+
if image_generation:
|
|
866
|
+
return []
|
|
867
|
+
|
|
868
|
+
url = "https://api.anthropic.com/v1/models"
|
|
869
|
+
|
|
870
|
+
try:
|
|
871
|
+
response = await self._request(method="GET", url=url)
|
|
872
|
+
except Exception as e:
|
|
873
|
+
logger.error(f"Failed to get available models for provider {self.name} due to exception: {e}")
|
|
874
|
+
return []
|
|
875
|
+
|
|
876
|
+
response_data = response.json().get("data", [])
|
|
877
|
+
all_models = [
|
|
878
|
+
ModelChangeSchema(
|
|
879
|
+
provider=self.name,
|
|
880
|
+
name=model.get("id"),
|
|
881
|
+
display_name=model.get("display_name"),
|
|
882
|
+
image_generation=False,
|
|
883
|
+
)
|
|
884
|
+
for model in response_data
|
|
885
|
+
if model.get("id") and model.get("type") == "model"
|
|
886
|
+
]
|
|
887
|
+
all_models.sort(key=lambda model: model.name)
|
|
888
|
+
|
|
889
|
+
if gpt_settings.models_whitelist:
|
|
890
|
+
return [model for model in all_models if model.name in gpt_settings.models_whitelist]
|
|
891
|
+
|
|
892
|
+
return all_models
|