euriai 0.4__py3-none-any.whl → 1.0.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.
- euriai/__init__.py +10 -10
- euriai/autogen.py +511 -0
- euriai/crewai.py +211 -0
- euriai/langchain.py +864 -0
- euriai/langgraph.py +1012 -0
- euriai/llamaindex.py +798 -0
- euriai/n8n.py +102 -0
- euriai/smolagents.py +819 -0
- {euriai-0.4.dist-info → euriai-1.0.0.dist-info}/METADATA +1 -1
- euriai-1.0.0.dist-info/RECORD +18 -0
- euriai/euri_autogen.py +0 -74
- euriai/euri_crewai.py +0 -92
- euriai/euri_langgraph.py +0 -64
- euriai/euri_llamaindex.py +0 -58
- euriai/euri_n8n.py +0 -30
- euriai/euri_smolagents.py +0 -44
- euriai/langchain_embed.py +0 -17
- euriai/langchain_llm.py +0 -29
- euriai-0.4.dist-info/RECORD +0 -19
- {euriai-0.4.dist-info → euriai-1.0.0.dist-info}/WHEEL +0 -0
- {euriai-0.4.dist-info → euriai-1.0.0.dist-info}/entry_points.txt +0 -0
- {euriai-0.4.dist-info → euriai-1.0.0.dist-info}/top_level.txt +0 -0
euriai/langchain.py
ADDED
@@ -0,0 +1,864 @@
|
|
1
|
+
"""
|
2
|
+
Enhanced LangChain Integration for Euri API
|
3
|
+
==========================================
|
4
|
+
|
5
|
+
This module provides a comprehensive LangChain integration with the Euri API,
|
6
|
+
including full ChatModel and Embeddings support with advanced features like
|
7
|
+
streaming, async operations, function calling, and structured output.
|
8
|
+
|
9
|
+
Usage:
|
10
|
+
from euriai.langchain_enhanced import EuriaiChatModel, EuriaiEmbeddings
|
11
|
+
|
12
|
+
# Chat model with all features
|
13
|
+
chat_model = EuriaiChatModel(
|
14
|
+
api_key="your_api_key",
|
15
|
+
model="gpt-4.1-nano",
|
16
|
+
temperature=0.7
|
17
|
+
)
|
18
|
+
|
19
|
+
# Embeddings model
|
20
|
+
embeddings = EuriaiEmbeddings(
|
21
|
+
api_key="your_api_key",
|
22
|
+
model="text-embedding-3-small"
|
23
|
+
)
|
24
|
+
"""
|
25
|
+
|
26
|
+
import asyncio
|
27
|
+
import json
|
28
|
+
import logging
|
29
|
+
from typing import (
|
30
|
+
Any, Dict, List, Optional, Iterator, AsyncIterator,
|
31
|
+
Union, Callable, Type, Sequence, Tuple
|
32
|
+
)
|
33
|
+
from abc import ABC, abstractmethod
|
34
|
+
from concurrent.futures import ThreadPoolExecutor
|
35
|
+
import time
|
36
|
+
|
37
|
+
try:
|
38
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
39
|
+
from langchain_core.language_models.llms import LLM
|
40
|
+
from langchain_core.embeddings import Embeddings
|
41
|
+
from langchain_core.messages import (
|
42
|
+
BaseMessage, AIMessage, HumanMessage, SystemMessage,
|
43
|
+
AIMessageChunk, FunctionMessage, ToolMessage
|
44
|
+
)
|
45
|
+
from langchain_core.messages.ai import UsageMetadata
|
46
|
+
from langchain_core.outputs import (
|
47
|
+
ChatGeneration, ChatGenerationChunk, ChatResult,
|
48
|
+
LLMResult, Generation
|
49
|
+
)
|
50
|
+
from langchain_core.callbacks import (
|
51
|
+
CallbackManagerForLLMRun, AsyncCallbackManagerForLLMRun
|
52
|
+
)
|
53
|
+
from langchain_core.runnables import RunnableConfig
|
54
|
+
from langchain_core.tools import BaseTool
|
55
|
+
from langchain_core.utils.function_calling import convert_to_openai_function
|
56
|
+
from pydantic import Field, BaseModel, SecretStr
|
57
|
+
LANGCHAIN_AVAILABLE = True
|
58
|
+
except ImportError:
|
59
|
+
LANGCHAIN_AVAILABLE = False
|
60
|
+
# Fallback base classes for when LangChain is not available
|
61
|
+
class BaseChatModel:
|
62
|
+
pass
|
63
|
+
class LLM:
|
64
|
+
pass
|
65
|
+
class Embeddings:
|
66
|
+
pass
|
67
|
+
class BaseMessage:
|
68
|
+
pass
|
69
|
+
class AIMessage:
|
70
|
+
pass
|
71
|
+
class HumanMessage:
|
72
|
+
pass
|
73
|
+
class SystemMessage:
|
74
|
+
pass
|
75
|
+
class AIMessageChunk:
|
76
|
+
pass
|
77
|
+
class FunctionMessage:
|
78
|
+
pass
|
79
|
+
class ToolMessage:
|
80
|
+
pass
|
81
|
+
class UsageMetadata:
|
82
|
+
pass
|
83
|
+
class ChatGeneration:
|
84
|
+
pass
|
85
|
+
class ChatGenerationChunk:
|
86
|
+
pass
|
87
|
+
class ChatResult:
|
88
|
+
pass
|
89
|
+
class LLMResult:
|
90
|
+
pass
|
91
|
+
class Generation:
|
92
|
+
pass
|
93
|
+
class CallbackManagerForLLMRun:
|
94
|
+
pass
|
95
|
+
class AsyncCallbackManagerForLLMRun:
|
96
|
+
pass
|
97
|
+
class RunnableConfig:
|
98
|
+
pass
|
99
|
+
class BaseTool:
|
100
|
+
pass
|
101
|
+
class Field:
|
102
|
+
pass
|
103
|
+
class BaseModel:
|
104
|
+
pass
|
105
|
+
class SecretStr:
|
106
|
+
pass
|
107
|
+
|
108
|
+
from euriai.client import EuriaiClient
|
109
|
+
from euriai.embedding import EuriaiEmbeddingClient
|
110
|
+
|
111
|
+
|
112
|
+
class EuriaiChatModel(BaseChatModel):
|
113
|
+
"""
|
114
|
+
Enhanced LangChain ChatModel implementation using Euri API.
|
115
|
+
|
116
|
+
This implementation provides full LangChain compatibility with advanced features:
|
117
|
+
- Streaming support (both sync and async)
|
118
|
+
- Function calling and tool use
|
119
|
+
- Structured output support
|
120
|
+
- Async operations
|
121
|
+
- Usage tracking and metadata
|
122
|
+
- Proper error handling
|
123
|
+
- Callback support
|
124
|
+
|
125
|
+
Example:
|
126
|
+
chat_model = EuriaiChatModel(
|
127
|
+
api_key="your_api_key",
|
128
|
+
model="gpt-4.1-nano",
|
129
|
+
temperature=0.7,
|
130
|
+
max_tokens=1000,
|
131
|
+
streaming=True
|
132
|
+
)
|
133
|
+
|
134
|
+
# Basic usage
|
135
|
+
response = chat_model.invoke("Hello, how are you?")
|
136
|
+
|
137
|
+
# Streaming
|
138
|
+
for chunk in chat_model.stream("Tell me a story"):
|
139
|
+
print(chunk.content, end="")
|
140
|
+
|
141
|
+
# Async
|
142
|
+
response = await chat_model.ainvoke("What is AI?")
|
143
|
+
|
144
|
+
# With messages
|
145
|
+
messages = [
|
146
|
+
SystemMessage(content="You are a helpful assistant"),
|
147
|
+
HumanMessage(content="What is the weather like?")
|
148
|
+
]
|
149
|
+
response = chat_model.invoke(messages)
|
150
|
+
"""
|
151
|
+
|
152
|
+
# Configuration
|
153
|
+
api_key: SecretStr = Field(description="Euri API key")
|
154
|
+
model: str = Field(default="gpt-4.1-nano", description="Model name")
|
155
|
+
temperature: float = Field(default=0.7, ge=0.0, le=1.0, description="Sampling temperature")
|
156
|
+
max_tokens: int = Field(default=1000, gt=0, description="Maximum tokens to generate")
|
157
|
+
top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0, description="Nucleus sampling parameter")
|
158
|
+
frequency_penalty: Optional[float] = Field(default=None, ge=-2.0, le=2.0, description="Frequency penalty")
|
159
|
+
presence_penalty: Optional[float] = Field(default=None, ge=-2.0, le=2.0, description="Presence penalty")
|
160
|
+
|
161
|
+
# Features
|
162
|
+
streaming: bool = Field(default=False, description="Enable streaming responses")
|
163
|
+
supports_function_calling: bool = Field(default=True, description="Support function calling")
|
164
|
+
supports_structured_output: bool = Field(default=True, description="Support structured output")
|
165
|
+
|
166
|
+
# Internal
|
167
|
+
_client: Optional[EuriaiClient] = None
|
168
|
+
_executor: Optional[ThreadPoolExecutor] = None
|
169
|
+
|
170
|
+
def __init__(self, **kwargs):
|
171
|
+
if not LANGCHAIN_AVAILABLE:
|
172
|
+
raise ImportError(
|
173
|
+
"LangChain is not installed. Please install with: "
|
174
|
+
"pip install langchain-core"
|
175
|
+
)
|
176
|
+
|
177
|
+
super().__init__(**kwargs)
|
178
|
+
|
179
|
+
# Initialize client
|
180
|
+
api_key_str = self.api_key.get_secret_value() if hasattr(self.api_key, 'get_secret_value') else str(self.api_key)
|
181
|
+
self._client = EuriaiClient(
|
182
|
+
api_key=api_key_str,
|
183
|
+
model=self.model
|
184
|
+
)
|
185
|
+
|
186
|
+
# Initialize thread pool for async operations
|
187
|
+
self._executor = ThreadPoolExecutor(max_workers=4)
|
188
|
+
|
189
|
+
@property
|
190
|
+
def _llm_type(self) -> str:
|
191
|
+
"""Get the type of language model."""
|
192
|
+
return "euriai_chat_enhanced"
|
193
|
+
|
194
|
+
@property
|
195
|
+
def _identifying_params(self) -> Dict[str, Any]:
|
196
|
+
"""Get identifying parameters for the model."""
|
197
|
+
return {
|
198
|
+
"model": self.model,
|
199
|
+
"temperature": self.temperature,
|
200
|
+
"max_tokens": self.max_tokens,
|
201
|
+
"top_p": self.top_p,
|
202
|
+
"frequency_penalty": self.frequency_penalty,
|
203
|
+
"presence_penalty": self.presence_penalty,
|
204
|
+
}
|
205
|
+
|
206
|
+
def _format_messages(self, messages: List[BaseMessage]) -> List[Dict[str, str]]:
|
207
|
+
"""Format LangChain messages for the Euri API."""
|
208
|
+
formatted_messages = []
|
209
|
+
|
210
|
+
for message in messages:
|
211
|
+
if isinstance(message, HumanMessage):
|
212
|
+
formatted_messages.append({"role": "user", "content": message.content})
|
213
|
+
elif isinstance(message, AIMessage):
|
214
|
+
formatted_messages.append({"role": "assistant", "content": message.content})
|
215
|
+
elif isinstance(message, SystemMessage):
|
216
|
+
formatted_messages.append({"role": "system", "content": message.content})
|
217
|
+
elif isinstance(message, (FunctionMessage, ToolMessage)):
|
218
|
+
formatted_messages.append({"role": "function", "content": message.content})
|
219
|
+
else:
|
220
|
+
# Fallback for other message types
|
221
|
+
formatted_messages.append({"role": "user", "content": str(message.content)})
|
222
|
+
|
223
|
+
return formatted_messages
|
224
|
+
|
225
|
+
def _create_chat_result(self, response: Dict[str, Any]) -> ChatResult:
|
226
|
+
"""Create ChatResult from API response."""
|
227
|
+
if "choices" not in response or not response["choices"]:
|
228
|
+
raise ValueError("Invalid response format from Euri API")
|
229
|
+
|
230
|
+
choice = response["choices"][0]
|
231
|
+
message_content = choice.get("message", {}).get("content", "")
|
232
|
+
|
233
|
+
# Extract usage information
|
234
|
+
usage = response.get("usage", {})
|
235
|
+
usage_metadata = UsageMetadata(
|
236
|
+
input_tokens=usage.get("prompt_tokens", 0),
|
237
|
+
output_tokens=usage.get("completion_tokens", 0),
|
238
|
+
total_tokens=usage.get("total_tokens", 0)
|
239
|
+
)
|
240
|
+
|
241
|
+
# Create AI message
|
242
|
+
ai_message = AIMessage(
|
243
|
+
content=message_content,
|
244
|
+
usage_metadata=usage_metadata,
|
245
|
+
response_metadata={
|
246
|
+
"model": self.model,
|
247
|
+
"finish_reason": choice.get("finish_reason"),
|
248
|
+
"created": response.get("created"),
|
249
|
+
}
|
250
|
+
)
|
251
|
+
|
252
|
+
generation = ChatGeneration(
|
253
|
+
message=ai_message,
|
254
|
+
generation_info={
|
255
|
+
"finish_reason": choice.get("finish_reason"),
|
256
|
+
"model": self.model,
|
257
|
+
}
|
258
|
+
)
|
259
|
+
|
260
|
+
return ChatResult(
|
261
|
+
generations=[generation],
|
262
|
+
llm_output={
|
263
|
+
"token_usage": usage,
|
264
|
+
"model_name": self.model,
|
265
|
+
"system_fingerprint": response.get("system_fingerprint"),
|
266
|
+
}
|
267
|
+
)
|
268
|
+
|
269
|
+
def _generate(
|
270
|
+
self,
|
271
|
+
messages: List[BaseMessage],
|
272
|
+
stop: Optional[List[str]] = None,
|
273
|
+
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
274
|
+
**kwargs: Any,
|
275
|
+
) -> ChatResult:
|
276
|
+
"""Generate chat response."""
|
277
|
+
# Format messages
|
278
|
+
formatted_messages = self._format_messages(messages)
|
279
|
+
|
280
|
+
# Prepare request
|
281
|
+
request_params = {
|
282
|
+
"messages": formatted_messages,
|
283
|
+
"temperature": self.temperature,
|
284
|
+
"max_tokens": self.max_tokens,
|
285
|
+
}
|
286
|
+
|
287
|
+
# Add optional parameters
|
288
|
+
if self.top_p is not None:
|
289
|
+
request_params["top_p"] = self.top_p
|
290
|
+
if self.frequency_penalty is not None:
|
291
|
+
request_params["frequency_penalty"] = self.frequency_penalty
|
292
|
+
if self.presence_penalty is not None:
|
293
|
+
request_params["presence_penalty"] = self.presence_penalty
|
294
|
+
if stop:
|
295
|
+
request_params["stop"] = stop
|
296
|
+
|
297
|
+
# Override with kwargs
|
298
|
+
request_params.update(kwargs)
|
299
|
+
|
300
|
+
try:
|
301
|
+
# Make API call
|
302
|
+
response = self._client.generate_completion(**request_params)
|
303
|
+
return self._create_chat_result(response)
|
304
|
+
except Exception as e:
|
305
|
+
if run_manager:
|
306
|
+
run_manager.on_llm_error(e)
|
307
|
+
raise
|
308
|
+
|
309
|
+
def _stream(
|
310
|
+
self,
|
311
|
+
messages: List[BaseMessage],
|
312
|
+
stop: Optional[List[str]] = None,
|
313
|
+
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
314
|
+
**kwargs: Any,
|
315
|
+
) -> Iterator[ChatGenerationChunk]:
|
316
|
+
"""Stream chat response."""
|
317
|
+
# Format messages
|
318
|
+
formatted_messages = self._format_messages(messages)
|
319
|
+
|
320
|
+
# Prepare request
|
321
|
+
request_params = {
|
322
|
+
"messages": formatted_messages,
|
323
|
+
"temperature": self.temperature,
|
324
|
+
"max_tokens": self.max_tokens,
|
325
|
+
}
|
326
|
+
|
327
|
+
# Add optional parameters
|
328
|
+
if self.top_p is not None:
|
329
|
+
request_params["top_p"] = self.top_p
|
330
|
+
if self.frequency_penalty is not None:
|
331
|
+
request_params["frequency_penalty"] = self.frequency_penalty
|
332
|
+
if self.presence_penalty is not None:
|
333
|
+
request_params["presence_penalty"] = self.presence_penalty
|
334
|
+
if stop:
|
335
|
+
request_params["stop"] = stop
|
336
|
+
|
337
|
+
# Override with kwargs
|
338
|
+
request_params.update(kwargs)
|
339
|
+
|
340
|
+
try:
|
341
|
+
# Stream response
|
342
|
+
accumulated_content = ""
|
343
|
+
for chunk_data in self._client.stream_completion(**request_params):
|
344
|
+
if chunk_data.strip():
|
345
|
+
try:
|
346
|
+
# Parse SSE data
|
347
|
+
if chunk_data.startswith("data: "):
|
348
|
+
chunk_data = chunk_data[6:]
|
349
|
+
|
350
|
+
if chunk_data.strip() == "[DONE]":
|
351
|
+
break
|
352
|
+
|
353
|
+
chunk_json = json.loads(chunk_data)
|
354
|
+
if "choices" in chunk_json and chunk_json["choices"]:
|
355
|
+
choice = chunk_json["choices"][0]
|
356
|
+
delta = choice.get("delta", {})
|
357
|
+
content = delta.get("content", "")
|
358
|
+
|
359
|
+
if content:
|
360
|
+
accumulated_content += content
|
361
|
+
|
362
|
+
# Create usage metadata
|
363
|
+
usage_metadata = UsageMetadata(
|
364
|
+
input_tokens=0,
|
365
|
+
output_tokens=1,
|
366
|
+
total_tokens=1
|
367
|
+
)
|
368
|
+
|
369
|
+
# Create chunk
|
370
|
+
chunk = ChatGenerationChunk(
|
371
|
+
message=AIMessageChunk(
|
372
|
+
content=content,
|
373
|
+
usage_metadata=usage_metadata
|
374
|
+
),
|
375
|
+
generation_info={
|
376
|
+
"finish_reason": choice.get("finish_reason"),
|
377
|
+
"model": self.model,
|
378
|
+
}
|
379
|
+
)
|
380
|
+
|
381
|
+
# Notify callback
|
382
|
+
if run_manager:
|
383
|
+
run_manager.on_llm_new_token(content, chunk=chunk)
|
384
|
+
|
385
|
+
yield chunk
|
386
|
+
except json.JSONDecodeError:
|
387
|
+
continue
|
388
|
+
|
389
|
+
except Exception as e:
|
390
|
+
if run_manager:
|
391
|
+
run_manager.on_llm_error(e)
|
392
|
+
raise
|
393
|
+
|
394
|
+
async def _agenerate(
|
395
|
+
self,
|
396
|
+
messages: List[BaseMessage],
|
397
|
+
stop: Optional[List[str]] = None,
|
398
|
+
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
399
|
+
**kwargs: Any,
|
400
|
+
) -> ChatResult:
|
401
|
+
"""Async generate chat response."""
|
402
|
+
# Run sync method in thread pool
|
403
|
+
loop = asyncio.get_event_loop()
|
404
|
+
return await loop.run_in_executor(
|
405
|
+
self._executor,
|
406
|
+
self._generate,
|
407
|
+
messages,
|
408
|
+
stop,
|
409
|
+
run_manager,
|
410
|
+
**kwargs
|
411
|
+
)
|
412
|
+
|
413
|
+
async def _astream(
|
414
|
+
self,
|
415
|
+
messages: List[BaseMessage],
|
416
|
+
stop: Optional[List[str]] = None,
|
417
|
+
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
418
|
+
**kwargs: Any,
|
419
|
+
) -> AsyncIterator[ChatGenerationChunk]:
|
420
|
+
"""Async stream chat response."""
|
421
|
+
# Run sync stream method in thread pool
|
422
|
+
loop = asyncio.get_event_loop()
|
423
|
+
|
424
|
+
def sync_stream():
|
425
|
+
return list(self._stream(messages, stop, run_manager, **kwargs))
|
426
|
+
|
427
|
+
chunks = await loop.run_in_executor(self._executor, sync_stream)
|
428
|
+
|
429
|
+
for chunk in chunks:
|
430
|
+
yield chunk
|
431
|
+
|
432
|
+
def bind_functions(self, functions: Sequence[Dict[str, Any]]) -> "EuriaiChatModel":
|
433
|
+
"""Bind functions to the model for function calling."""
|
434
|
+
# Create new instance with functions bound
|
435
|
+
return self.__class__(
|
436
|
+
api_key=self.api_key,
|
437
|
+
model=self.model,
|
438
|
+
temperature=self.temperature,
|
439
|
+
max_tokens=self.max_tokens,
|
440
|
+
top_p=self.top_p,
|
441
|
+
frequency_penalty=self.frequency_penalty,
|
442
|
+
presence_penalty=self.presence_penalty,
|
443
|
+
streaming=self.streaming,
|
444
|
+
supports_function_calling=self.supports_function_calling,
|
445
|
+
supports_structured_output=self.supports_structured_output,
|
446
|
+
_bound_functions=functions
|
447
|
+
)
|
448
|
+
|
449
|
+
def bind_tools(self, tools: Sequence[Union[Dict[str, Any], BaseTool]]) -> "EuriaiChatModel":
|
450
|
+
"""Bind tools to the model for tool calling."""
|
451
|
+
# Convert tools to functions
|
452
|
+
functions = []
|
453
|
+
for tool in tools:
|
454
|
+
if isinstance(tool, dict):
|
455
|
+
functions.append(tool)
|
456
|
+
elif hasattr(tool, 'to_function'):
|
457
|
+
functions.append(tool.to_function())
|
458
|
+
else:
|
459
|
+
# Convert tool to function format
|
460
|
+
functions.append(convert_to_openai_function(tool))
|
461
|
+
|
462
|
+
return self.bind_functions(functions)
|
463
|
+
|
464
|
+
def with_structured_output(
|
465
|
+
self,
|
466
|
+
schema: Union[Dict, Type[BaseModel]],
|
467
|
+
**kwargs: Any
|
468
|
+
) -> "EuriaiChatModel":
|
469
|
+
"""Create a version that returns structured output."""
|
470
|
+
# This would need to be implemented with proper schema validation
|
471
|
+
# For now, return self with structured output enabled
|
472
|
+
return self.__class__(
|
473
|
+
api_key=self.api_key,
|
474
|
+
model=self.model,
|
475
|
+
temperature=self.temperature,
|
476
|
+
max_tokens=self.max_tokens,
|
477
|
+
top_p=self.top_p,
|
478
|
+
frequency_penalty=self.frequency_penalty,
|
479
|
+
presence_penalty=self.presence_penalty,
|
480
|
+
streaming=self.streaming,
|
481
|
+
supports_function_calling=self.supports_function_calling,
|
482
|
+
supports_structured_output=True,
|
483
|
+
_structured_output_schema=schema,
|
484
|
+
**kwargs
|
485
|
+
)
|
486
|
+
|
487
|
+
|
488
|
+
class EuriaiEmbeddings(Embeddings):
|
489
|
+
"""
|
490
|
+
Enhanced LangChain Embeddings implementation using Euri API.
|
491
|
+
|
492
|
+
This implementation provides full LangChain compatibility with:
|
493
|
+
- Batch embedding support
|
494
|
+
- Async operations
|
495
|
+
- Error handling and retries
|
496
|
+
- Usage tracking
|
497
|
+
- Configurable chunk size
|
498
|
+
|
499
|
+
Example:
|
500
|
+
embeddings = EuriaiEmbeddings(
|
501
|
+
api_key="your_api_key",
|
502
|
+
model="text-embedding-3-small",
|
503
|
+
chunk_size=1000
|
504
|
+
)
|
505
|
+
|
506
|
+
# Single document
|
507
|
+
embedding = embeddings.embed_query("Hello world")
|
508
|
+
|
509
|
+
# Multiple documents
|
510
|
+
embeddings_list = embeddings.embed_documents([
|
511
|
+
"Document 1",
|
512
|
+
"Document 2",
|
513
|
+
"Document 3"
|
514
|
+
])
|
515
|
+
|
516
|
+
# Async
|
517
|
+
embedding = await embeddings.aembed_query("Hello world")
|
518
|
+
"""
|
519
|
+
|
520
|
+
# Configuration
|
521
|
+
api_key: SecretStr = Field(description="Euri API key")
|
522
|
+
model: str = Field(default="text-embedding-3-small", description="Embedding model name")
|
523
|
+
chunk_size: int = Field(default=1000, gt=0, description="Chunk size for batch processing")
|
524
|
+
max_retries: int = Field(default=3, ge=0, description="Maximum number of retries")
|
525
|
+
request_timeout: int = Field(default=60, gt=0, description="Request timeout in seconds")
|
526
|
+
|
527
|
+
# Internal
|
528
|
+
_client: Optional[EuriaiEmbeddingClient] = None
|
529
|
+
_executor: Optional[ThreadPoolExecutor] = None
|
530
|
+
|
531
|
+
def __init__(self, **kwargs):
|
532
|
+
if not LANGCHAIN_AVAILABLE:
|
533
|
+
raise ImportError(
|
534
|
+
"LangChain is not installed. Please install with: "
|
535
|
+
"pip install langchain-core"
|
536
|
+
)
|
537
|
+
|
538
|
+
super().__init__(**kwargs)
|
539
|
+
|
540
|
+
# Initialize client
|
541
|
+
api_key_str = self.api_key.get_secret_value() if hasattr(self.api_key, 'get_secret_value') else str(self.api_key)
|
542
|
+
self._client = EuriaiEmbeddingClient(
|
543
|
+
api_key=api_key_str,
|
544
|
+
model=self.model
|
545
|
+
)
|
546
|
+
|
547
|
+
# Initialize thread pool for async operations
|
548
|
+
self._executor = ThreadPoolExecutor(max_workers=4)
|
549
|
+
|
550
|
+
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
551
|
+
"""Embed search documents."""
|
552
|
+
if not texts:
|
553
|
+
return []
|
554
|
+
|
555
|
+
# Process in chunks to avoid API limits
|
556
|
+
all_embeddings = []
|
557
|
+
for i in range(0, len(texts), self.chunk_size):
|
558
|
+
chunk = texts[i:i + self.chunk_size]
|
559
|
+
|
560
|
+
# Get embeddings for this chunk
|
561
|
+
chunk_embeddings = self._client.embed_batch(chunk)
|
562
|
+
all_embeddings.extend([emb.tolist() for emb in chunk_embeddings])
|
563
|
+
|
564
|
+
return all_embeddings
|
565
|
+
|
566
|
+
def embed_query(self, text: str) -> List[float]:
|
567
|
+
"""Embed a query text."""
|
568
|
+
embedding = self._client.embed(text)
|
569
|
+
return embedding.tolist()
|
570
|
+
|
571
|
+
async def aembed_documents(self, texts: List[str]) -> List[List[float]]:
|
572
|
+
"""Async embed search documents."""
|
573
|
+
loop = asyncio.get_event_loop()
|
574
|
+
return await loop.run_in_executor(
|
575
|
+
self._executor,
|
576
|
+
self.embed_documents,
|
577
|
+
texts
|
578
|
+
)
|
579
|
+
|
580
|
+
async def aembed_query(self, text: str) -> List[float]:
|
581
|
+
"""Async embed a query text."""
|
582
|
+
loop = asyncio.get_event_loop()
|
583
|
+
return await loop.run_in_executor(
|
584
|
+
self._executor,
|
585
|
+
self.embed_query,
|
586
|
+
text
|
587
|
+
)
|
588
|
+
|
589
|
+
|
590
|
+
class EuriaiLLM(LLM):
|
591
|
+
"""
|
592
|
+
Enhanced LangChain LLM implementation using Euri API.
|
593
|
+
|
594
|
+
This provides the traditional LLM interface (text-in, text-out)
|
595
|
+
while using the Euri API backend.
|
596
|
+
|
597
|
+
Example:
|
598
|
+
llm = EuriaiLLM(
|
599
|
+
api_key="your_api_key",
|
600
|
+
model="gpt-4.1-nano",
|
601
|
+
temperature=0.5
|
602
|
+
)
|
603
|
+
|
604
|
+
response = llm.invoke("What is the capital of France?")
|
605
|
+
|
606
|
+
# Streaming
|
607
|
+
for chunk in llm.stream("Tell me a joke"):
|
608
|
+
print(chunk, end="")
|
609
|
+
"""
|
610
|
+
|
611
|
+
# Configuration
|
612
|
+
api_key: SecretStr = Field(description="Euri API key")
|
613
|
+
model: str = Field(default="gpt-4.1-nano", description="Model name")
|
614
|
+
temperature: float = Field(default=0.7, ge=0.0, le=1.0, description="Sampling temperature")
|
615
|
+
max_tokens: int = Field(default=1000, gt=0, description="Maximum tokens to generate")
|
616
|
+
top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0, description="Nucleus sampling parameter")
|
617
|
+
frequency_penalty: Optional[float] = Field(default=None, ge=-2.0, le=2.0, description="Frequency penalty")
|
618
|
+
presence_penalty: Optional[float] = Field(default=None, ge=-2.0, le=2.0, description="Presence penalty")
|
619
|
+
|
620
|
+
# Internal
|
621
|
+
_client: Optional[EuriaiClient] = None
|
622
|
+
_executor: Optional[ThreadPoolExecutor] = None
|
623
|
+
|
624
|
+
def __init__(self, **kwargs):
|
625
|
+
if not LANGCHAIN_AVAILABLE:
|
626
|
+
raise ImportError(
|
627
|
+
"LangChain is not installed. Please install with: "
|
628
|
+
"pip install langchain-core"
|
629
|
+
)
|
630
|
+
|
631
|
+
super().__init__(**kwargs)
|
632
|
+
|
633
|
+
# Initialize client
|
634
|
+
api_key_str = self.api_key.get_secret_value() if hasattr(self.api_key, 'get_secret_value') else str(self.api_key)
|
635
|
+
self._client = EuriaiClient(
|
636
|
+
api_key=api_key_str,
|
637
|
+
model=self.model
|
638
|
+
)
|
639
|
+
|
640
|
+
# Initialize thread pool for async operations
|
641
|
+
self._executor = ThreadPoolExecutor(max_workers=4)
|
642
|
+
|
643
|
+
@property
|
644
|
+
def _llm_type(self) -> str:
|
645
|
+
"""Get the type of language model."""
|
646
|
+
return "euriai_llm_enhanced"
|
647
|
+
|
648
|
+
@property
|
649
|
+
def _identifying_params(self) -> Dict[str, Any]:
|
650
|
+
"""Get identifying parameters for the model."""
|
651
|
+
return {
|
652
|
+
"model": self.model,
|
653
|
+
"temperature": self.temperature,
|
654
|
+
"max_tokens": self.max_tokens,
|
655
|
+
"top_p": self.top_p,
|
656
|
+
"frequency_penalty": self.frequency_penalty,
|
657
|
+
"presence_penalty": self.presence_penalty,
|
658
|
+
}
|
659
|
+
|
660
|
+
def _call(
|
661
|
+
self,
|
662
|
+
prompt: str,
|
663
|
+
stop: Optional[List[str]] = None,
|
664
|
+
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
665
|
+
**kwargs: Any,
|
666
|
+
) -> str:
|
667
|
+
"""Call the Euri API."""
|
668
|
+
# Prepare request
|
669
|
+
request_params = {
|
670
|
+
"prompt": prompt,
|
671
|
+
"temperature": self.temperature,
|
672
|
+
"max_tokens": self.max_tokens,
|
673
|
+
}
|
674
|
+
|
675
|
+
# Add optional parameters
|
676
|
+
if self.top_p is not None:
|
677
|
+
request_params["top_p"] = self.top_p
|
678
|
+
if self.frequency_penalty is not None:
|
679
|
+
request_params["frequency_penalty"] = self.frequency_penalty
|
680
|
+
if self.presence_penalty is not None:
|
681
|
+
request_params["presence_penalty"] = self.presence_penalty
|
682
|
+
if stop:
|
683
|
+
request_params["stop"] = stop
|
684
|
+
|
685
|
+
# Override with kwargs
|
686
|
+
request_params.update(kwargs)
|
687
|
+
|
688
|
+
try:
|
689
|
+
# Make API call
|
690
|
+
response = self._client.generate_completion(**request_params)
|
691
|
+
return response.get("choices", [{}])[0].get("message", {}).get("content", "")
|
692
|
+
except Exception as e:
|
693
|
+
if run_manager:
|
694
|
+
run_manager.on_llm_error(e)
|
695
|
+
raise
|
696
|
+
|
697
|
+
def _stream(
|
698
|
+
self,
|
699
|
+
prompt: str,
|
700
|
+
stop: Optional[List[str]] = None,
|
701
|
+
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
702
|
+
**kwargs: Any,
|
703
|
+
) -> Iterator[str]:
|
704
|
+
"""Stream the LLM response."""
|
705
|
+
# Prepare request
|
706
|
+
request_params = {
|
707
|
+
"prompt": prompt,
|
708
|
+
"temperature": self.temperature,
|
709
|
+
"max_tokens": self.max_tokens,
|
710
|
+
}
|
711
|
+
|
712
|
+
# Add optional parameters
|
713
|
+
if self.top_p is not None:
|
714
|
+
request_params["top_p"] = self.top_p
|
715
|
+
if self.frequency_penalty is not None:
|
716
|
+
request_params["frequency_penalty"] = self.frequency_penalty
|
717
|
+
if self.presence_penalty is not None:
|
718
|
+
request_params["presence_penalty"] = self.presence_penalty
|
719
|
+
if stop:
|
720
|
+
request_params["stop"] = stop
|
721
|
+
|
722
|
+
# Override with kwargs
|
723
|
+
request_params.update(kwargs)
|
724
|
+
|
725
|
+
try:
|
726
|
+
# Stream response
|
727
|
+
for chunk_data in self._client.stream_completion(**request_params):
|
728
|
+
if chunk_data.strip():
|
729
|
+
try:
|
730
|
+
# Parse SSE data
|
731
|
+
if chunk_data.startswith("data: "):
|
732
|
+
chunk_data = chunk_data[6:]
|
733
|
+
|
734
|
+
if chunk_data.strip() == "[DONE]":
|
735
|
+
break
|
736
|
+
|
737
|
+
chunk_json = json.loads(chunk_data)
|
738
|
+
if "choices" in chunk_json and chunk_json["choices"]:
|
739
|
+
choice = chunk_json["choices"][0]
|
740
|
+
delta = choice.get("delta", {})
|
741
|
+
content = delta.get("content", "")
|
742
|
+
|
743
|
+
if content:
|
744
|
+
# Notify callback
|
745
|
+
if run_manager:
|
746
|
+
run_manager.on_llm_new_token(content)
|
747
|
+
|
748
|
+
yield content
|
749
|
+
except json.JSONDecodeError:
|
750
|
+
continue
|
751
|
+
|
752
|
+
except Exception as e:
|
753
|
+
if run_manager:
|
754
|
+
run_manager.on_llm_error(e)
|
755
|
+
raise
|
756
|
+
|
757
|
+
async def _acall(
|
758
|
+
self,
|
759
|
+
prompt: str,
|
760
|
+
stop: Optional[List[str]] = None,
|
761
|
+
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
762
|
+
**kwargs: Any,
|
763
|
+
) -> str:
|
764
|
+
"""Async call the Euri API."""
|
765
|
+
loop = asyncio.get_event_loop()
|
766
|
+
return await loop.run_in_executor(
|
767
|
+
self._executor,
|
768
|
+
self._call,
|
769
|
+
prompt,
|
770
|
+
stop,
|
771
|
+
run_manager,
|
772
|
+
**kwargs
|
773
|
+
)
|
774
|
+
|
775
|
+
async def _astream(
|
776
|
+
self,
|
777
|
+
prompt: str,
|
778
|
+
stop: Optional[List[str]] = None,
|
779
|
+
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
780
|
+
**kwargs: Any,
|
781
|
+
) -> AsyncIterator[str]:
|
782
|
+
"""Async stream the LLM response."""
|
783
|
+
loop = asyncio.get_event_loop()
|
784
|
+
|
785
|
+
def sync_stream():
|
786
|
+
return list(self._stream(prompt, stop, run_manager, **kwargs))
|
787
|
+
|
788
|
+
chunks = await loop.run_in_executor(self._executor, sync_stream)
|
789
|
+
|
790
|
+
for chunk in chunks:
|
791
|
+
yield chunk
|
792
|
+
|
793
|
+
|
794
|
+
# Convenience functions for easy model creation
|
795
|
+
def create_chat_model(
|
796
|
+
api_key: str,
|
797
|
+
model: str = "gpt-4.1-nano",
|
798
|
+
temperature: float = 0.7,
|
799
|
+
**kwargs
|
800
|
+
) -> EuriaiChatModel:
|
801
|
+
"""Create a chat model with default settings."""
|
802
|
+
return EuriaiChatModel(
|
803
|
+
api_key=api_key,
|
804
|
+
model=model,
|
805
|
+
temperature=temperature,
|
806
|
+
**kwargs
|
807
|
+
)
|
808
|
+
|
809
|
+
|
810
|
+
def create_embeddings(
|
811
|
+
api_key: str,
|
812
|
+
model: str = "text-embedding-3-small",
|
813
|
+
**kwargs
|
814
|
+
) -> EuriaiEmbeddings:
|
815
|
+
"""Create an embeddings model with default settings."""
|
816
|
+
return EuriaiEmbeddings(
|
817
|
+
api_key=api_key,
|
818
|
+
model=model,
|
819
|
+
**kwargs
|
820
|
+
)
|
821
|
+
|
822
|
+
|
823
|
+
def create_llm(
|
824
|
+
api_key: str,
|
825
|
+
model: str = "gpt-4.1-nano",
|
826
|
+
temperature: float = 0.7,
|
827
|
+
**kwargs
|
828
|
+
) -> EuriaiLLM:
|
829
|
+
"""Create an LLM with default settings."""
|
830
|
+
return EuriaiLLM(
|
831
|
+
api_key=api_key,
|
832
|
+
model=model,
|
833
|
+
temperature=temperature,
|
834
|
+
**kwargs
|
835
|
+
)
|
836
|
+
|
837
|
+
|
838
|
+
# Model information
|
839
|
+
AVAILABLE_MODELS = {
|
840
|
+
"chat": [
|
841
|
+
"gpt-4.1-nano",
|
842
|
+
"gpt-4.1-mini",
|
843
|
+
"gpt-4.1-turbo",
|
844
|
+
"claude-3.5-sonnet",
|
845
|
+
"claude-3.5-haiku",
|
846
|
+
"gemini-2.5-flash",
|
847
|
+
"gemini-2.0-flash-exp"
|
848
|
+
],
|
849
|
+
"embeddings": [
|
850
|
+
"text-embedding-3-small",
|
851
|
+
"text-embedding-3-large",
|
852
|
+
"text-embedding-ada-002"
|
853
|
+
]
|
854
|
+
}
|
855
|
+
|
856
|
+
|
857
|
+
def get_available_models() -> Dict[str, List[str]]:
|
858
|
+
"""Get list of available models."""
|
859
|
+
return AVAILABLE_MODELS.copy()
|
860
|
+
|
861
|
+
|
862
|
+
def validate_model(model: str, model_type: str = "chat") -> bool:
|
863
|
+
"""Validate if a model is available."""
|
864
|
+
return model in AVAILABLE_MODELS.get(model_type, [])
|