chatterer 0.1.17__py3-none-any.whl → 0.1.19__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.
- chatterer/__init__.py +93 -93
- chatterer/common_types/__init__.py +21 -21
- chatterer/common_types/io.py +19 -19
- chatterer/examples/__init__.py +0 -0
- chatterer/examples/anything_to_markdown.py +95 -0
- chatterer/examples/get_code_snippets.py +64 -0
- chatterer/examples/login_with_playwright.py +171 -0
- chatterer/examples/make_ppt.py +499 -0
- chatterer/examples/pdf_to_markdown.py +107 -0
- chatterer/examples/pdf_to_text.py +60 -0
- chatterer/examples/transcription_api.py +127 -0
- chatterer/examples/upstage_parser.py +95 -0
- chatterer/examples/webpage_to_markdown.py +79 -0
- chatterer/interactive.py +354 -354
- chatterer/language_model.py +533 -533
- chatterer/messages.py +21 -21
- chatterer/strategies/__init__.py +13 -13
- chatterer/strategies/atom_of_thoughts.py +975 -975
- chatterer/strategies/base.py +14 -14
- chatterer/tools/__init__.py +46 -46
- chatterer/tools/caption_markdown_images.py +384 -384
- chatterer/tools/citation_chunking/__init__.py +3 -3
- chatterer/tools/citation_chunking/chunks.py +53 -53
- chatterer/tools/citation_chunking/citation_chunker.py +118 -118
- chatterer/tools/citation_chunking/citations.py +285 -285
- chatterer/tools/citation_chunking/prompt.py +157 -157
- chatterer/tools/citation_chunking/reference.py +26 -26
- chatterer/tools/citation_chunking/utils.py +138 -138
- chatterer/tools/convert_pdf_to_markdown.py +302 -302
- chatterer/tools/convert_to_text.py +447 -447
- chatterer/tools/upstage_document_parser.py +705 -705
- chatterer/tools/webpage_to_markdown.py +739 -739
- chatterer/tools/youtube.py +146 -146
- chatterer/utils/__init__.py +15 -15
- chatterer/utils/base64_image.py +285 -285
- chatterer/utils/bytesio.py +59 -59
- chatterer/utils/code_agent.py +237 -237
- chatterer/utils/imghdr.py +148 -148
- {chatterer-0.1.17.dist-info → chatterer-0.1.19.dist-info}/METADATA +392 -392
- chatterer-0.1.19.dist-info/RECORD +44 -0
- {chatterer-0.1.17.dist-info → chatterer-0.1.19.dist-info}/WHEEL +1 -1
- chatterer-0.1.19.dist-info/entry_points.txt +10 -0
- chatterer-0.1.17.dist-info/RECORD +0 -33
- {chatterer-0.1.17.dist-info → chatterer-0.1.19.dist-info}/top_level.txt +0 -0
chatterer/language_model.py
CHANGED
@@ -1,533 +1,533 @@
|
|
1
|
-
import re
|
2
|
-
from typing import (
|
3
|
-
TYPE_CHECKING,
|
4
|
-
Any,
|
5
|
-
AsyncIterator,
|
6
|
-
Callable,
|
7
|
-
Iterable,
|
8
|
-
Iterator,
|
9
|
-
Literal,
|
10
|
-
Optional,
|
11
|
-
Self,
|
12
|
-
Sequence,
|
13
|
-
Type,
|
14
|
-
TypeAlias,
|
15
|
-
TypeVar,
|
16
|
-
overload,
|
17
|
-
)
|
18
|
-
|
19
|
-
from langchain_core.language_models.base import LanguageModelInput
|
20
|
-
from langchain_core.language_models.chat_models import BaseChatModel
|
21
|
-
from langchain_core.runnables.base import Runnable
|
22
|
-
from langchain_core.runnables.config import RunnableConfig
|
23
|
-
from langchain_core.utils.utils import secret_from_env
|
24
|
-
from pydantic import BaseModel, Field, SecretStr
|
25
|
-
|
26
|
-
from .messages import AIMessage, BaseMessage, HumanMessage, UsageMetadata
|
27
|
-
from .utils.code_agent import CodeExecutionResult, FunctionSignature, augment_prompt_for_toolcall
|
28
|
-
|
29
|
-
if TYPE_CHECKING:
|
30
|
-
from instructor import Partial
|
31
|
-
from langchain_experimental.tools.python.tool import PythonAstREPLTool
|
32
|
-
|
33
|
-
PydanticModelT = TypeVar("PydanticModelT", bound=BaseModel)
|
34
|
-
StructuredOutputType: TypeAlias = dict[object, object] | BaseModel
|
35
|
-
|
36
|
-
DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION = "Provide a detailed description of all visible elements in the image, summarizing key details in a few clear sentences."
|
37
|
-
DEFAULT_CODE_GENERATION_PROMPT = (
|
38
|
-
"You are utilizing a Python code execution tool now.\n"
|
39
|
-
"Your goal is to generate Python code that solves the task efficiently and appends both the code and its output to your context memory.\n"
|
40
|
-
"\n"
|
41
|
-
"To optimize tool efficiency, follow these guidelines:\n"
|
42
|
-
"- Write concise, efficient code that directly serves the intended purpose.\n"
|
43
|
-
"- Avoid unnecessary operations (e.g., excessive loops, recursion, or heavy computations).\n"
|
44
|
-
"- Handle potential errors gracefully (e.g., using try-except blocks).\n"
|
45
|
-
"\n"
|
46
|
-
"Return your response strictly in the following JSON format:\n"
|
47
|
-
'{\n "code": "<your_python_code_here>"\n}\n\n'
|
48
|
-
)
|
49
|
-
|
50
|
-
|
51
|
-
DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT = (
|
52
|
-
"Below functions are included in global scope and can be used in your code.\n"
|
53
|
-
"Do not try to redefine the function(s).\n"
|
54
|
-
"You don't have to force yourself to use these tools - use them only when you need to.\n"
|
55
|
-
)
|
56
|
-
DEFAULT_FUNCTION_REFERENCE_SEPARATOR = "\n---\n" # Separator to distinguish different function references
|
57
|
-
|
58
|
-
PYTHON_CODE_PATTERN: re.Pattern[str] = re.compile(r"```(?:python\s*\n)?(.*?)```", re.DOTALL)
|
59
|
-
|
60
|
-
|
61
|
-
class Chatterer(BaseModel):
|
62
|
-
"""Language model for generating text from a given input."""
|
63
|
-
|
64
|
-
client: BaseChatModel
|
65
|
-
structured_output_kwargs: dict[str, Any] = Field(default_factory=dict)
|
66
|
-
|
67
|
-
@classmethod
|
68
|
-
def from_provider(
|
69
|
-
cls, provider_and_model: str, structured_output_kwargs: Optional[dict[str, object]] = {"strict": True}
|
70
|
-
) -> Self:
|
71
|
-
backend, model = provider_and_model.split(":", 1)
|
72
|
-
backends = cls.get_backends()
|
73
|
-
if func := backends.get(backend):
|
74
|
-
return func(model, structured_output_kwargs)
|
75
|
-
else:
|
76
|
-
raise ValueError(f"Unsupported provider: {backend}. Supported providers are: {', '.join(backends.keys())}.")
|
77
|
-
|
78
|
-
@classmethod
|
79
|
-
def get_backends(cls) -> dict[str, Callable[[str, Optional[dict[str, object]]], Self]]:
|
80
|
-
return {
|
81
|
-
"openai": cls.openai,
|
82
|
-
"anthropic": cls.anthropic,
|
83
|
-
"google": cls.google,
|
84
|
-
"ollama": cls.ollama,
|
85
|
-
"openrouter": cls.open_router,
|
86
|
-
"xai": cls.xai,
|
87
|
-
}
|
88
|
-
|
89
|
-
@classmethod
|
90
|
-
def openai(
|
91
|
-
cls,
|
92
|
-
model: str = "gpt-4.1",
|
93
|
-
structured_output_kwargs: Optional[dict[str, object]] = {"strict": True},
|
94
|
-
api_key: Optional[str] = None,
|
95
|
-
**kwargs: Any,
|
96
|
-
) -> Self:
|
97
|
-
from langchain_openai import ChatOpenAI
|
98
|
-
|
99
|
-
return cls(
|
100
|
-
client=ChatOpenAI(
|
101
|
-
model=model,
|
102
|
-
api_key=_get_api_key(api_key=api_key, env_key="OPENAI_API_KEY", raise_if_none=False),
|
103
|
-
**kwargs,
|
104
|
-
),
|
105
|
-
structured_output_kwargs=structured_output_kwargs or {},
|
106
|
-
)
|
107
|
-
|
108
|
-
@classmethod
|
109
|
-
def anthropic(
|
110
|
-
cls,
|
111
|
-
model_name: str = "claude-3-7-sonnet-20250219",
|
112
|
-
structured_output_kwargs: Optional[dict[str, object]] = None,
|
113
|
-
api_key: Optional[str] = None,
|
114
|
-
**kwargs: Any,
|
115
|
-
) -> Self:
|
116
|
-
from langchain_anthropic import ChatAnthropic
|
117
|
-
|
118
|
-
return cls(
|
119
|
-
client=ChatAnthropic(
|
120
|
-
model_name=model_name,
|
121
|
-
api_key=_get_api_key(api_key=api_key, env_key="ANTHROPIC_API_KEY", raise_if_none=True),
|
122
|
-
**kwargs,
|
123
|
-
),
|
124
|
-
structured_output_kwargs=structured_output_kwargs or {},
|
125
|
-
)
|
126
|
-
|
127
|
-
@classmethod
|
128
|
-
def google(
|
129
|
-
cls,
|
130
|
-
model: str = "gemini-2.5-flash-preview-04-17",
|
131
|
-
structured_output_kwargs: Optional[dict[str, object]] = None,
|
132
|
-
api_key: Optional[str] = None,
|
133
|
-
**kwargs: Any,
|
134
|
-
) -> Self:
|
135
|
-
from langchain_google_genai import ChatGoogleGenerativeAI
|
136
|
-
|
137
|
-
return cls(
|
138
|
-
client=ChatGoogleGenerativeAI(
|
139
|
-
model=model,
|
140
|
-
api_key=_get_api_key(api_key=api_key, env_key="GOOGLE_API_KEY", raise_if_none=False),
|
141
|
-
**kwargs,
|
142
|
-
),
|
143
|
-
structured_output_kwargs=structured_output_kwargs or {},
|
144
|
-
)
|
145
|
-
|
146
|
-
@classmethod
|
147
|
-
def ollama(
|
148
|
-
cls,
|
149
|
-
model: str = "deepseek-r1:1.5b",
|
150
|
-
structured_output_kwargs: Optional[dict[str, object]] = None,
|
151
|
-
**kwargs: Any,
|
152
|
-
) -> Self:
|
153
|
-
from langchain_ollama import ChatOllama
|
154
|
-
|
155
|
-
return cls(
|
156
|
-
client=ChatOllama(model=model, **kwargs),
|
157
|
-
structured_output_kwargs=structured_output_kwargs or {},
|
158
|
-
)
|
159
|
-
|
160
|
-
@classmethod
|
161
|
-
def open_router(
|
162
|
-
cls,
|
163
|
-
model: str = "openrouter/quasar-alpha",
|
164
|
-
structured_output_kwargs: Optional[dict[str, object]] = None,
|
165
|
-
api_key: Optional[str] = None,
|
166
|
-
**kwargs: Any,
|
167
|
-
) -> Self:
|
168
|
-
from langchain_openai import ChatOpenAI
|
169
|
-
|
170
|
-
return cls(
|
171
|
-
client=ChatOpenAI(
|
172
|
-
model=model,
|
173
|
-
base_url="https://openrouter.ai/api/v1",
|
174
|
-
api_key=_get_api_key(api_key=api_key, env_key="OPENROUTER_API_KEY", raise_if_none=False),
|
175
|
-
**kwargs,
|
176
|
-
),
|
177
|
-
structured_output_kwargs=structured_output_kwargs or {},
|
178
|
-
)
|
179
|
-
|
180
|
-
@classmethod
|
181
|
-
def xai(
|
182
|
-
cls,
|
183
|
-
model: str = "grok-3-mini",
|
184
|
-
structured_output_kwargs: Optional[dict[str, object]] = None,
|
185
|
-
base_url: str = "https://api.x.ai/v1",
|
186
|
-
api_key: Optional[str] = None,
|
187
|
-
**kwargs: Any,
|
188
|
-
) -> Self:
|
189
|
-
from langchain_openai import ChatOpenAI
|
190
|
-
|
191
|
-
return cls(
|
192
|
-
client=ChatOpenAI(
|
193
|
-
model=model,
|
194
|
-
base_url=base_url,
|
195
|
-
api_key=_get_api_key(api_key=api_key, env_key="XAI_API_KEY", raise_if_none=False),
|
196
|
-
**kwargs,
|
197
|
-
),
|
198
|
-
structured_output_kwargs=structured_output_kwargs or {},
|
199
|
-
)
|
200
|
-
|
201
|
-
@property
|
202
|
-
def invoke(self):
|
203
|
-
return self.client.invoke
|
204
|
-
|
205
|
-
@property
|
206
|
-
def ainvoke(self):
|
207
|
-
return self.client.ainvoke
|
208
|
-
|
209
|
-
@property
|
210
|
-
def stream(self):
|
211
|
-
return self.client.stream
|
212
|
-
|
213
|
-
@property
|
214
|
-
def astream(self):
|
215
|
-
return self.client.astream
|
216
|
-
|
217
|
-
@property
|
218
|
-
def bind_tools(self): # pyright: ignore[reportUnknownParameterType]
|
219
|
-
return self.client.bind_tools # pyright: ignore[reportUnknownParameterType, reportUnknownVariableType, reportUnknownMemberType]
|
220
|
-
|
221
|
-
def __getattr__(self, name: str) -> Any:
|
222
|
-
return getattr(self.client, name)
|
223
|
-
|
224
|
-
@overload
|
225
|
-
def __call__(
|
226
|
-
self,
|
227
|
-
messages: LanguageModelInput,
|
228
|
-
response_model: Type[PydanticModelT],
|
229
|
-
config: Optional[RunnableConfig] = None,
|
230
|
-
stop: Optional[list[str]] = None,
|
231
|
-
**kwargs: Any,
|
232
|
-
) -> PydanticModelT: ...
|
233
|
-
|
234
|
-
@overload
|
235
|
-
def __call__(
|
236
|
-
self,
|
237
|
-
messages: LanguageModelInput,
|
238
|
-
response_model: None = None,
|
239
|
-
config: Optional[RunnableConfig] = None,
|
240
|
-
stop: Optional[list[str]] = None,
|
241
|
-
**kwargs: Any,
|
242
|
-
) -> str: ...
|
243
|
-
|
244
|
-
def __call__(
|
245
|
-
self,
|
246
|
-
messages: LanguageModelInput,
|
247
|
-
response_model: Optional[Type[PydanticModelT]] = None,
|
248
|
-
config: Optional[RunnableConfig] = None,
|
249
|
-
stop: Optional[list[str]] = None,
|
250
|
-
**kwargs: Any,
|
251
|
-
) -> str | PydanticModelT:
|
252
|
-
if response_model:
|
253
|
-
return self.generate_pydantic(response_model, messages, config, stop, **kwargs)
|
254
|
-
return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
|
255
|
-
|
256
|
-
def generate(
|
257
|
-
self,
|
258
|
-
messages: LanguageModelInput,
|
259
|
-
config: Optional[RunnableConfig] = None,
|
260
|
-
stop: Optional[list[str]] = None,
|
261
|
-
**kwargs: Any,
|
262
|
-
) -> str:
|
263
|
-
return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
|
264
|
-
|
265
|
-
async def agenerate(
|
266
|
-
self,
|
267
|
-
messages: LanguageModelInput,
|
268
|
-
config: Optional[RunnableConfig] = None,
|
269
|
-
stop: Optional[list[str]] = None,
|
270
|
-
**kwargs: Any,
|
271
|
-
) -> str:
|
272
|
-
return (await self.client.ainvoke(input=messages, config=config, stop=stop, **kwargs)).text()
|
273
|
-
|
274
|
-
def generate_stream(
|
275
|
-
self,
|
276
|
-
messages: LanguageModelInput,
|
277
|
-
config: Optional[RunnableConfig] = None,
|
278
|
-
stop: Optional[list[str]] = None,
|
279
|
-
**kwargs: Any,
|
280
|
-
) -> Iterator[str]:
|
281
|
-
for chunk in self.client.stream(input=messages, config=config, stop=stop, **kwargs):
|
282
|
-
yield chunk.text()
|
283
|
-
|
284
|
-
async def agenerate_stream(
|
285
|
-
self,
|
286
|
-
messages: LanguageModelInput,
|
287
|
-
config: Optional[RunnableConfig] = None,
|
288
|
-
stop: Optional[list[str]] = None,
|
289
|
-
**kwargs: Any,
|
290
|
-
) -> AsyncIterator[str]:
|
291
|
-
async for chunk in self.client.astream(input=messages, config=config, stop=stop, **kwargs):
|
292
|
-
yield chunk.text()
|
293
|
-
|
294
|
-
def generate_pydantic(
|
295
|
-
self,
|
296
|
-
response_model: Type[PydanticModelT],
|
297
|
-
messages: LanguageModelInput,
|
298
|
-
config: Optional[RunnableConfig] = None,
|
299
|
-
stop: Optional[list[str]] = None,
|
300
|
-
**kwargs: Any,
|
301
|
-
) -> PydanticModelT:
|
302
|
-
result: StructuredOutputType = _with_structured_output(
|
303
|
-
client=self.client,
|
304
|
-
response_model=response_model,
|
305
|
-
structured_output_kwargs=self.structured_output_kwargs,
|
306
|
-
).invoke(input=messages, config=config, stop=stop, **kwargs)
|
307
|
-
if isinstance(result, response_model):
|
308
|
-
return result
|
309
|
-
else:
|
310
|
-
return response_model.model_validate(result)
|
311
|
-
|
312
|
-
async def agenerate_pydantic(
|
313
|
-
self,
|
314
|
-
response_model: Type[PydanticModelT],
|
315
|
-
messages: LanguageModelInput,
|
316
|
-
config: Optional[RunnableConfig] = None,
|
317
|
-
stop: Optional[list[str]] = None,
|
318
|
-
**kwargs: Any,
|
319
|
-
) -> PydanticModelT:
|
320
|
-
result: StructuredOutputType = await _with_structured_output(
|
321
|
-
client=self.client,
|
322
|
-
response_model=response_model,
|
323
|
-
structured_output_kwargs=self.structured_output_kwargs,
|
324
|
-
).ainvoke(input=messages, config=config, stop=stop, **kwargs)
|
325
|
-
if isinstance(result, response_model):
|
326
|
-
return result
|
327
|
-
else:
|
328
|
-
return response_model.model_validate(result)
|
329
|
-
|
330
|
-
def generate_pydantic_stream(
|
331
|
-
self,
|
332
|
-
response_model: Type[PydanticModelT],
|
333
|
-
messages: LanguageModelInput,
|
334
|
-
config: Optional[RunnableConfig] = None,
|
335
|
-
stop: Optional[list[str]] = None,
|
336
|
-
**kwargs: Any,
|
337
|
-
) -> Iterator[PydanticModelT]:
|
338
|
-
try:
|
339
|
-
import instructor
|
340
|
-
except ImportError:
|
341
|
-
raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
|
342
|
-
|
343
|
-
partial_response_model = instructor.Partial[response_model]
|
344
|
-
for chunk in _with_structured_output(
|
345
|
-
client=self.client,
|
346
|
-
response_model=partial_response_model,
|
347
|
-
structured_output_kwargs=self.structured_output_kwargs,
|
348
|
-
).stream(input=messages, config=config, stop=stop, **kwargs):
|
349
|
-
yield response_model.model_validate(chunk)
|
350
|
-
|
351
|
-
async def agenerate_pydantic_stream(
|
352
|
-
self,
|
353
|
-
response_model: Type[PydanticModelT],
|
354
|
-
messages: LanguageModelInput,
|
355
|
-
config: Optional[RunnableConfig] = None,
|
356
|
-
stop: Optional[list[str]] = None,
|
357
|
-
**kwargs: Any,
|
358
|
-
) -> AsyncIterator[PydanticModelT]:
|
359
|
-
try:
|
360
|
-
import instructor
|
361
|
-
except ImportError:
|
362
|
-
raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
|
363
|
-
|
364
|
-
partial_response_model = instructor.Partial[response_model]
|
365
|
-
async for chunk in _with_structured_output(
|
366
|
-
client=self.client,
|
367
|
-
response_model=partial_response_model,
|
368
|
-
structured_output_kwargs=self.structured_output_kwargs,
|
369
|
-
).astream(input=messages, config=config, stop=stop, **kwargs):
|
370
|
-
yield response_model.model_validate(chunk)
|
371
|
-
|
372
|
-
def describe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
|
373
|
-
"""
|
374
|
-
Create a detailed description of an image using the Vision Language Model.
|
375
|
-
- image_url: Image URL to describe
|
376
|
-
"""
|
377
|
-
return self.generate([
|
378
|
-
HumanMessage(
|
379
|
-
content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
|
380
|
-
)
|
381
|
-
])
|
382
|
-
|
383
|
-
async def adescribe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
|
384
|
-
"""
|
385
|
-
Create a detailed description of an image using the Vision Language Model asynchronously.
|
386
|
-
- image_url: Image URL to describe
|
387
|
-
"""
|
388
|
-
return await self.agenerate([
|
389
|
-
HumanMessage(
|
390
|
-
content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
|
391
|
-
)
|
392
|
-
])
|
393
|
-
|
394
|
-
def get_approximate_token_count(self, message: BaseMessage) -> int:
|
395
|
-
return self.client.get_num_tokens_from_messages([message]) # pyright: ignore[reportUnknownMemberType]
|
396
|
-
|
397
|
-
def get_usage_metadata(self, message: BaseMessage) -> UsageMetadata:
|
398
|
-
if isinstance(message, AIMessage):
|
399
|
-
usage_metadata = message.usage_metadata
|
400
|
-
if usage_metadata is not None:
|
401
|
-
input_tokens = usage_metadata["input_tokens"]
|
402
|
-
output_tokens = usage_metadata["output_tokens"]
|
403
|
-
return {
|
404
|
-
"input_tokens": input_tokens,
|
405
|
-
"output_tokens": output_tokens,
|
406
|
-
"total_tokens": input_tokens + output_tokens,
|
407
|
-
}
|
408
|
-
else:
|
409
|
-
approx_tokens = self.get_approximate_token_count(message)
|
410
|
-
return {"input_tokens": 0, "output_tokens": approx_tokens, "total_tokens": approx_tokens}
|
411
|
-
else:
|
412
|
-
approx_tokens = self.get_approximate_token_count(message)
|
413
|
-
return {
|
414
|
-
"input_tokens": approx_tokens,
|
415
|
-
"output_tokens": 0,
|
416
|
-
"total_tokens": approx_tokens,
|
417
|
-
}
|
418
|
-
|
419
|
-
def exec(
|
420
|
-
self,
|
421
|
-
messages: LanguageModelInput,
|
422
|
-
repl_tool: Optional["PythonAstREPLTool"] = None,
|
423
|
-
prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
|
424
|
-
function_signatures: Optional[FunctionSignature | Iterable[FunctionSignature]] = None,
|
425
|
-
function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
|
426
|
-
function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
|
427
|
-
config: Optional[RunnableConfig] = None,
|
428
|
-
stop: Optional[list[str]] = None,
|
429
|
-
**kwargs: Any,
|
430
|
-
) -> CodeExecutionResult:
|
431
|
-
if not function_signatures:
|
432
|
-
function_signatures = []
|
433
|
-
elif isinstance(function_signatures, FunctionSignature):
|
434
|
-
function_signatures = [function_signatures]
|
435
|
-
messages = augment_prompt_for_toolcall(
|
436
|
-
function_signatures=function_signatures,
|
437
|
-
messages=messages,
|
438
|
-
prompt_for_code_invoke=prompt_for_code_invoke,
|
439
|
-
function_reference_prefix=function_reference_prefix,
|
440
|
-
function_reference_seperator=function_reference_seperator,
|
441
|
-
)
|
442
|
-
code_obj: PythonCodeToExecute = self.generate_pydantic(
|
443
|
-
response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
|
444
|
-
)
|
445
|
-
return CodeExecutionResult.from_code(
|
446
|
-
code=code_obj.code,
|
447
|
-
config=config,
|
448
|
-
repl_tool=repl_tool,
|
449
|
-
function_signatures=function_signatures,
|
450
|
-
**kwargs,
|
451
|
-
)
|
452
|
-
|
453
|
-
@property
|
454
|
-
def invoke_code_execution(self) -> Callable[..., CodeExecutionResult]:
|
455
|
-
"""Alias for exec method for backward compatibility."""
|
456
|
-
return self.exec
|
457
|
-
|
458
|
-
async def aexec(
|
459
|
-
self,
|
460
|
-
messages: LanguageModelInput,
|
461
|
-
repl_tool: Optional["PythonAstREPLTool"] = None,
|
462
|
-
prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
|
463
|
-
additional_callables: Optional[Callable[..., object] | Sequence[Callable[..., object]]] = None,
|
464
|
-
function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
|
465
|
-
function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
|
466
|
-
config: Optional[RunnableConfig] = None,
|
467
|
-
stop: Optional[list[str]] = None,
|
468
|
-
**kwargs: Any,
|
469
|
-
) -> CodeExecutionResult:
|
470
|
-
function_signatures: list[FunctionSignature] = FunctionSignature.from_callable(additional_callables)
|
471
|
-
messages = augment_prompt_for_toolcall(
|
472
|
-
function_signatures=function_signatures,
|
473
|
-
messages=messages,
|
474
|
-
prompt_for_code_invoke=prompt_for_code_invoke,
|
475
|
-
function_reference_prefix=function_reference_prefix,
|
476
|
-
function_reference_seperator=function_reference_seperator,
|
477
|
-
)
|
478
|
-
code_obj: PythonCodeToExecute = await self.agenerate_pydantic(
|
479
|
-
response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
|
480
|
-
)
|
481
|
-
return await CodeExecutionResult.afrom_code(
|
482
|
-
code=code_obj.code,
|
483
|
-
config=config,
|
484
|
-
repl_tool=repl_tool,
|
485
|
-
function_signatures=function_signatures,
|
486
|
-
**kwargs,
|
487
|
-
)
|
488
|
-
|
489
|
-
@property
|
490
|
-
def ainvoke_code_execution(self):
|
491
|
-
"""Alias for aexec method for backward compatibility."""
|
492
|
-
return self.aexec
|
493
|
-
|
494
|
-
|
495
|
-
class PythonCodeToExecute(BaseModel):
|
496
|
-
code: str = Field(description="Python code to execute")
|
497
|
-
|
498
|
-
def model_post_init(self, context: object) -> None:
|
499
|
-
super().model_post_init(context)
|
500
|
-
|
501
|
-
codes: list[str] = []
|
502
|
-
for match in PYTHON_CODE_PATTERN.finditer(self.code):
|
503
|
-
codes.append(match.group(1))
|
504
|
-
if codes:
|
505
|
-
self.code = "\n".join(codes)
|
506
|
-
|
507
|
-
|
508
|
-
def _with_structured_output(
|
509
|
-
client: BaseChatModel,
|
510
|
-
response_model: Type["PydanticModelT | Partial[PydanticModelT]"],
|
511
|
-
structured_output_kwargs: dict[str, Any],
|
512
|
-
) -> Runnable[LanguageModelInput, dict[object, object] | BaseModel]:
|
513
|
-
return client.with_structured_output(schema=response_model, **structured_output_kwargs) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
514
|
-
|
515
|
-
|
516
|
-
@overload
|
517
|
-
def _get_api_key(api_key: Optional[str], env_key: str, raise_if_none: Literal[True]) -> SecretStr: ...
|
518
|
-
@overload
|
519
|
-
def _get_api_key(api_key: Optional[str], env_key: str, raise_if_none: Literal[False]) -> Optional[SecretStr]: ...
|
520
|
-
def _get_api_key(api_key: Optional[str], env_key: str, raise_if_none: bool) -> Optional[SecretStr]:
|
521
|
-
if api_key is None:
|
522
|
-
api_key_found: SecretStr | None = secret_from_env(env_key, default=None)()
|
523
|
-
if raise_if_none and api_key_found is None:
|
524
|
-
raise ValueError(
|
525
|
-
(
|
526
|
-
f"Did not find API key, please add an environment variable"
|
527
|
-
f" `{env_key}` which contains it, or pass"
|
528
|
-
f" api_key as a named parameter."
|
529
|
-
)
|
530
|
-
)
|
531
|
-
return api_key_found
|
532
|
-
else:
|
533
|
-
return SecretStr(api_key)
|
1
|
+
import re
|
2
|
+
from typing import (
|
3
|
+
TYPE_CHECKING,
|
4
|
+
Any,
|
5
|
+
AsyncIterator,
|
6
|
+
Callable,
|
7
|
+
Iterable,
|
8
|
+
Iterator,
|
9
|
+
Literal,
|
10
|
+
Optional,
|
11
|
+
Self,
|
12
|
+
Sequence,
|
13
|
+
Type,
|
14
|
+
TypeAlias,
|
15
|
+
TypeVar,
|
16
|
+
overload,
|
17
|
+
)
|
18
|
+
|
19
|
+
from langchain_core.language_models.base import LanguageModelInput
|
20
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
21
|
+
from langchain_core.runnables.base import Runnable
|
22
|
+
from langchain_core.runnables.config import RunnableConfig
|
23
|
+
from langchain_core.utils.utils import secret_from_env
|
24
|
+
from pydantic import BaseModel, Field, SecretStr
|
25
|
+
|
26
|
+
from .messages import AIMessage, BaseMessage, HumanMessage, UsageMetadata
|
27
|
+
from .utils.code_agent import CodeExecutionResult, FunctionSignature, augment_prompt_for_toolcall
|
28
|
+
|
29
|
+
if TYPE_CHECKING:
|
30
|
+
from instructor import Partial
|
31
|
+
from langchain_experimental.tools.python.tool import PythonAstREPLTool
|
32
|
+
|
33
|
+
PydanticModelT = TypeVar("PydanticModelT", bound=BaseModel)
|
34
|
+
StructuredOutputType: TypeAlias = dict[object, object] | BaseModel
|
35
|
+
|
36
|
+
DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION = "Provide a detailed description of all visible elements in the image, summarizing key details in a few clear sentences."
|
37
|
+
DEFAULT_CODE_GENERATION_PROMPT = (
|
38
|
+
"You are utilizing a Python code execution tool now.\n"
|
39
|
+
"Your goal is to generate Python code that solves the task efficiently and appends both the code and its output to your context memory.\n"
|
40
|
+
"\n"
|
41
|
+
"To optimize tool efficiency, follow these guidelines:\n"
|
42
|
+
"- Write concise, efficient code that directly serves the intended purpose.\n"
|
43
|
+
"- Avoid unnecessary operations (e.g., excessive loops, recursion, or heavy computations).\n"
|
44
|
+
"- Handle potential errors gracefully (e.g., using try-except blocks).\n"
|
45
|
+
"\n"
|
46
|
+
"Return your response strictly in the following JSON format:\n"
|
47
|
+
'{\n "code": "<your_python_code_here>"\n}\n\n'
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT = (
|
52
|
+
"Below functions are included in global scope and can be used in your code.\n"
|
53
|
+
"Do not try to redefine the function(s).\n"
|
54
|
+
"You don't have to force yourself to use these tools - use them only when you need to.\n"
|
55
|
+
)
|
56
|
+
DEFAULT_FUNCTION_REFERENCE_SEPARATOR = "\n---\n" # Separator to distinguish different function references
|
57
|
+
|
58
|
+
PYTHON_CODE_PATTERN: re.Pattern[str] = re.compile(r"```(?:python\s*\n)?(.*?)```", re.DOTALL)
|
59
|
+
|
60
|
+
|
61
|
+
class Chatterer(BaseModel):
|
62
|
+
"""Language model for generating text from a given input."""
|
63
|
+
|
64
|
+
client: BaseChatModel
|
65
|
+
structured_output_kwargs: dict[str, Any] = Field(default_factory=dict)
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def from_provider(
|
69
|
+
cls, provider_and_model: str, structured_output_kwargs: Optional[dict[str, object]] = {"strict": True}
|
70
|
+
) -> Self:
|
71
|
+
backend, model = provider_and_model.split(":", 1)
|
72
|
+
backends = cls.get_backends()
|
73
|
+
if func := backends.get(backend):
|
74
|
+
return func(model, structured_output_kwargs)
|
75
|
+
else:
|
76
|
+
raise ValueError(f"Unsupported provider: {backend}. Supported providers are: {', '.join(backends.keys())}.")
|
77
|
+
|
78
|
+
@classmethod
|
79
|
+
def get_backends(cls) -> dict[str, Callable[[str, Optional[dict[str, object]]], Self]]:
|
80
|
+
return {
|
81
|
+
"openai": cls.openai,
|
82
|
+
"anthropic": cls.anthropic,
|
83
|
+
"google": cls.google,
|
84
|
+
"ollama": cls.ollama,
|
85
|
+
"openrouter": cls.open_router,
|
86
|
+
"xai": cls.xai,
|
87
|
+
}
|
88
|
+
|
89
|
+
@classmethod
|
90
|
+
def openai(
|
91
|
+
cls,
|
92
|
+
model: str = "gpt-4.1",
|
93
|
+
structured_output_kwargs: Optional[dict[str, object]] = {"strict": True},
|
94
|
+
api_key: Optional[str] = None,
|
95
|
+
**kwargs: Any,
|
96
|
+
) -> Self:
|
97
|
+
from langchain_openai import ChatOpenAI
|
98
|
+
|
99
|
+
return cls(
|
100
|
+
client=ChatOpenAI(
|
101
|
+
model=model,
|
102
|
+
api_key=_get_api_key(api_key=api_key, env_key="OPENAI_API_KEY", raise_if_none=False),
|
103
|
+
**kwargs,
|
104
|
+
),
|
105
|
+
structured_output_kwargs=structured_output_kwargs or {},
|
106
|
+
)
|
107
|
+
|
108
|
+
@classmethod
|
109
|
+
def anthropic(
|
110
|
+
cls,
|
111
|
+
model_name: str = "claude-3-7-sonnet-20250219",
|
112
|
+
structured_output_kwargs: Optional[dict[str, object]] = None,
|
113
|
+
api_key: Optional[str] = None,
|
114
|
+
**kwargs: Any,
|
115
|
+
) -> Self:
|
116
|
+
from langchain_anthropic import ChatAnthropic
|
117
|
+
|
118
|
+
return cls(
|
119
|
+
client=ChatAnthropic(
|
120
|
+
model_name=model_name,
|
121
|
+
api_key=_get_api_key(api_key=api_key, env_key="ANTHROPIC_API_KEY", raise_if_none=True),
|
122
|
+
**kwargs,
|
123
|
+
),
|
124
|
+
structured_output_kwargs=structured_output_kwargs or {},
|
125
|
+
)
|
126
|
+
|
127
|
+
@classmethod
|
128
|
+
def google(
|
129
|
+
cls,
|
130
|
+
model: str = "gemini-2.5-flash-preview-04-17",
|
131
|
+
structured_output_kwargs: Optional[dict[str, object]] = None,
|
132
|
+
api_key: Optional[str] = None,
|
133
|
+
**kwargs: Any,
|
134
|
+
) -> Self:
|
135
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
136
|
+
|
137
|
+
return cls(
|
138
|
+
client=ChatGoogleGenerativeAI(
|
139
|
+
model=model,
|
140
|
+
api_key=_get_api_key(api_key=api_key, env_key="GOOGLE_API_KEY", raise_if_none=False),
|
141
|
+
**kwargs,
|
142
|
+
),
|
143
|
+
structured_output_kwargs=structured_output_kwargs or {},
|
144
|
+
)
|
145
|
+
|
146
|
+
@classmethod
|
147
|
+
def ollama(
|
148
|
+
cls,
|
149
|
+
model: str = "deepseek-r1:1.5b",
|
150
|
+
structured_output_kwargs: Optional[dict[str, object]] = None,
|
151
|
+
**kwargs: Any,
|
152
|
+
) -> Self:
|
153
|
+
from langchain_ollama import ChatOllama
|
154
|
+
|
155
|
+
return cls(
|
156
|
+
client=ChatOllama(model=model, **kwargs),
|
157
|
+
structured_output_kwargs=structured_output_kwargs or {},
|
158
|
+
)
|
159
|
+
|
160
|
+
@classmethod
|
161
|
+
def open_router(
|
162
|
+
cls,
|
163
|
+
model: str = "openrouter/quasar-alpha",
|
164
|
+
structured_output_kwargs: Optional[dict[str, object]] = None,
|
165
|
+
api_key: Optional[str] = None,
|
166
|
+
**kwargs: Any,
|
167
|
+
) -> Self:
|
168
|
+
from langchain_openai import ChatOpenAI
|
169
|
+
|
170
|
+
return cls(
|
171
|
+
client=ChatOpenAI(
|
172
|
+
model=model,
|
173
|
+
base_url="https://openrouter.ai/api/v1",
|
174
|
+
api_key=_get_api_key(api_key=api_key, env_key="OPENROUTER_API_KEY", raise_if_none=False),
|
175
|
+
**kwargs,
|
176
|
+
),
|
177
|
+
structured_output_kwargs=structured_output_kwargs or {},
|
178
|
+
)
|
179
|
+
|
180
|
+
@classmethod
|
181
|
+
def xai(
|
182
|
+
cls,
|
183
|
+
model: str = "grok-3-mini",
|
184
|
+
structured_output_kwargs: Optional[dict[str, object]] = None,
|
185
|
+
base_url: str = "https://api.x.ai/v1",
|
186
|
+
api_key: Optional[str] = None,
|
187
|
+
**kwargs: Any,
|
188
|
+
) -> Self:
|
189
|
+
from langchain_openai import ChatOpenAI
|
190
|
+
|
191
|
+
return cls(
|
192
|
+
client=ChatOpenAI(
|
193
|
+
model=model,
|
194
|
+
base_url=base_url,
|
195
|
+
api_key=_get_api_key(api_key=api_key, env_key="XAI_API_KEY", raise_if_none=False),
|
196
|
+
**kwargs,
|
197
|
+
),
|
198
|
+
structured_output_kwargs=structured_output_kwargs or {},
|
199
|
+
)
|
200
|
+
|
201
|
+
@property
|
202
|
+
def invoke(self):
|
203
|
+
return self.client.invoke
|
204
|
+
|
205
|
+
@property
|
206
|
+
def ainvoke(self):
|
207
|
+
return self.client.ainvoke
|
208
|
+
|
209
|
+
@property
|
210
|
+
def stream(self):
|
211
|
+
return self.client.stream
|
212
|
+
|
213
|
+
@property
|
214
|
+
def astream(self):
|
215
|
+
return self.client.astream
|
216
|
+
|
217
|
+
@property
|
218
|
+
def bind_tools(self): # pyright: ignore[reportUnknownParameterType]
|
219
|
+
return self.client.bind_tools # pyright: ignore[reportUnknownParameterType, reportUnknownVariableType, reportUnknownMemberType]
|
220
|
+
|
221
|
+
def __getattr__(self, name: str) -> Any:
|
222
|
+
return getattr(self.client, name)
|
223
|
+
|
224
|
+
@overload
|
225
|
+
def __call__(
|
226
|
+
self,
|
227
|
+
messages: LanguageModelInput,
|
228
|
+
response_model: Type[PydanticModelT],
|
229
|
+
config: Optional[RunnableConfig] = None,
|
230
|
+
stop: Optional[list[str]] = None,
|
231
|
+
**kwargs: Any,
|
232
|
+
) -> PydanticModelT: ...
|
233
|
+
|
234
|
+
@overload
|
235
|
+
def __call__(
|
236
|
+
self,
|
237
|
+
messages: LanguageModelInput,
|
238
|
+
response_model: None = None,
|
239
|
+
config: Optional[RunnableConfig] = None,
|
240
|
+
stop: Optional[list[str]] = None,
|
241
|
+
**kwargs: Any,
|
242
|
+
) -> str: ...
|
243
|
+
|
244
|
+
def __call__(
|
245
|
+
self,
|
246
|
+
messages: LanguageModelInput,
|
247
|
+
response_model: Optional[Type[PydanticModelT]] = None,
|
248
|
+
config: Optional[RunnableConfig] = None,
|
249
|
+
stop: Optional[list[str]] = None,
|
250
|
+
**kwargs: Any,
|
251
|
+
) -> str | PydanticModelT:
|
252
|
+
if response_model:
|
253
|
+
return self.generate_pydantic(response_model, messages, config, stop, **kwargs)
|
254
|
+
return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
|
255
|
+
|
256
|
+
def generate(
|
257
|
+
self,
|
258
|
+
messages: LanguageModelInput,
|
259
|
+
config: Optional[RunnableConfig] = None,
|
260
|
+
stop: Optional[list[str]] = None,
|
261
|
+
**kwargs: Any,
|
262
|
+
) -> str:
|
263
|
+
return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
|
264
|
+
|
265
|
+
async def agenerate(
|
266
|
+
self,
|
267
|
+
messages: LanguageModelInput,
|
268
|
+
config: Optional[RunnableConfig] = None,
|
269
|
+
stop: Optional[list[str]] = None,
|
270
|
+
**kwargs: Any,
|
271
|
+
) -> str:
|
272
|
+
return (await self.client.ainvoke(input=messages, config=config, stop=stop, **kwargs)).text()
|
273
|
+
|
274
|
+
def generate_stream(
|
275
|
+
self,
|
276
|
+
messages: LanguageModelInput,
|
277
|
+
config: Optional[RunnableConfig] = None,
|
278
|
+
stop: Optional[list[str]] = None,
|
279
|
+
**kwargs: Any,
|
280
|
+
) -> Iterator[str]:
|
281
|
+
for chunk in self.client.stream(input=messages, config=config, stop=stop, **kwargs):
|
282
|
+
yield chunk.text()
|
283
|
+
|
284
|
+
async def agenerate_stream(
|
285
|
+
self,
|
286
|
+
messages: LanguageModelInput,
|
287
|
+
config: Optional[RunnableConfig] = None,
|
288
|
+
stop: Optional[list[str]] = None,
|
289
|
+
**kwargs: Any,
|
290
|
+
) -> AsyncIterator[str]:
|
291
|
+
async for chunk in self.client.astream(input=messages, config=config, stop=stop, **kwargs):
|
292
|
+
yield chunk.text()
|
293
|
+
|
294
|
+
def generate_pydantic(
|
295
|
+
self,
|
296
|
+
response_model: Type[PydanticModelT],
|
297
|
+
messages: LanguageModelInput,
|
298
|
+
config: Optional[RunnableConfig] = None,
|
299
|
+
stop: Optional[list[str]] = None,
|
300
|
+
**kwargs: Any,
|
301
|
+
) -> PydanticModelT:
|
302
|
+
result: StructuredOutputType = _with_structured_output(
|
303
|
+
client=self.client,
|
304
|
+
response_model=response_model,
|
305
|
+
structured_output_kwargs=self.structured_output_kwargs,
|
306
|
+
).invoke(input=messages, config=config, stop=stop, **kwargs)
|
307
|
+
if isinstance(result, response_model):
|
308
|
+
return result
|
309
|
+
else:
|
310
|
+
return response_model.model_validate(result)
|
311
|
+
|
312
|
+
async def agenerate_pydantic(
|
313
|
+
self,
|
314
|
+
response_model: Type[PydanticModelT],
|
315
|
+
messages: LanguageModelInput,
|
316
|
+
config: Optional[RunnableConfig] = None,
|
317
|
+
stop: Optional[list[str]] = None,
|
318
|
+
**kwargs: Any,
|
319
|
+
) -> PydanticModelT:
|
320
|
+
result: StructuredOutputType = await _with_structured_output(
|
321
|
+
client=self.client,
|
322
|
+
response_model=response_model,
|
323
|
+
structured_output_kwargs=self.structured_output_kwargs,
|
324
|
+
).ainvoke(input=messages, config=config, stop=stop, **kwargs)
|
325
|
+
if isinstance(result, response_model):
|
326
|
+
return result
|
327
|
+
else:
|
328
|
+
return response_model.model_validate(result)
|
329
|
+
|
330
|
+
def generate_pydantic_stream(
|
331
|
+
self,
|
332
|
+
response_model: Type[PydanticModelT],
|
333
|
+
messages: LanguageModelInput,
|
334
|
+
config: Optional[RunnableConfig] = None,
|
335
|
+
stop: Optional[list[str]] = None,
|
336
|
+
**kwargs: Any,
|
337
|
+
) -> Iterator[PydanticModelT]:
|
338
|
+
try:
|
339
|
+
import instructor
|
340
|
+
except ImportError:
|
341
|
+
raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
|
342
|
+
|
343
|
+
partial_response_model = instructor.Partial[response_model]
|
344
|
+
for chunk in _with_structured_output(
|
345
|
+
client=self.client,
|
346
|
+
response_model=partial_response_model,
|
347
|
+
structured_output_kwargs=self.structured_output_kwargs,
|
348
|
+
).stream(input=messages, config=config, stop=stop, **kwargs):
|
349
|
+
yield response_model.model_validate(chunk)
|
350
|
+
|
351
|
+
async def agenerate_pydantic_stream(
|
352
|
+
self,
|
353
|
+
response_model: Type[PydanticModelT],
|
354
|
+
messages: LanguageModelInput,
|
355
|
+
config: Optional[RunnableConfig] = None,
|
356
|
+
stop: Optional[list[str]] = None,
|
357
|
+
**kwargs: Any,
|
358
|
+
) -> AsyncIterator[PydanticModelT]:
|
359
|
+
try:
|
360
|
+
import instructor
|
361
|
+
except ImportError:
|
362
|
+
raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
|
363
|
+
|
364
|
+
partial_response_model = instructor.Partial[response_model]
|
365
|
+
async for chunk in _with_structured_output(
|
366
|
+
client=self.client,
|
367
|
+
response_model=partial_response_model,
|
368
|
+
structured_output_kwargs=self.structured_output_kwargs,
|
369
|
+
).astream(input=messages, config=config, stop=stop, **kwargs):
|
370
|
+
yield response_model.model_validate(chunk)
|
371
|
+
|
372
|
+
def describe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
|
373
|
+
"""
|
374
|
+
Create a detailed description of an image using the Vision Language Model.
|
375
|
+
- image_url: Image URL to describe
|
376
|
+
"""
|
377
|
+
return self.generate([
|
378
|
+
HumanMessage(
|
379
|
+
content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
|
380
|
+
)
|
381
|
+
])
|
382
|
+
|
383
|
+
async def adescribe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
|
384
|
+
"""
|
385
|
+
Create a detailed description of an image using the Vision Language Model asynchronously.
|
386
|
+
- image_url: Image URL to describe
|
387
|
+
"""
|
388
|
+
return await self.agenerate([
|
389
|
+
HumanMessage(
|
390
|
+
content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
|
391
|
+
)
|
392
|
+
])
|
393
|
+
|
394
|
+
def get_approximate_token_count(self, message: BaseMessage) -> int:
|
395
|
+
return self.client.get_num_tokens_from_messages([message]) # pyright: ignore[reportUnknownMemberType]
|
396
|
+
|
397
|
+
def get_usage_metadata(self, message: BaseMessage) -> UsageMetadata:
|
398
|
+
if isinstance(message, AIMessage):
|
399
|
+
usage_metadata = message.usage_metadata
|
400
|
+
if usage_metadata is not None:
|
401
|
+
input_tokens = usage_metadata["input_tokens"]
|
402
|
+
output_tokens = usage_metadata["output_tokens"]
|
403
|
+
return {
|
404
|
+
"input_tokens": input_tokens,
|
405
|
+
"output_tokens": output_tokens,
|
406
|
+
"total_tokens": input_tokens + output_tokens,
|
407
|
+
}
|
408
|
+
else:
|
409
|
+
approx_tokens = self.get_approximate_token_count(message)
|
410
|
+
return {"input_tokens": 0, "output_tokens": approx_tokens, "total_tokens": approx_tokens}
|
411
|
+
else:
|
412
|
+
approx_tokens = self.get_approximate_token_count(message)
|
413
|
+
return {
|
414
|
+
"input_tokens": approx_tokens,
|
415
|
+
"output_tokens": 0,
|
416
|
+
"total_tokens": approx_tokens,
|
417
|
+
}
|
418
|
+
|
419
|
+
def exec(
|
420
|
+
self,
|
421
|
+
messages: LanguageModelInput,
|
422
|
+
repl_tool: Optional["PythonAstREPLTool"] = None,
|
423
|
+
prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
|
424
|
+
function_signatures: Optional[FunctionSignature | Iterable[FunctionSignature]] = None,
|
425
|
+
function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
|
426
|
+
function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
|
427
|
+
config: Optional[RunnableConfig] = None,
|
428
|
+
stop: Optional[list[str]] = None,
|
429
|
+
**kwargs: Any,
|
430
|
+
) -> CodeExecutionResult:
|
431
|
+
if not function_signatures:
|
432
|
+
function_signatures = []
|
433
|
+
elif isinstance(function_signatures, FunctionSignature):
|
434
|
+
function_signatures = [function_signatures]
|
435
|
+
messages = augment_prompt_for_toolcall(
|
436
|
+
function_signatures=function_signatures,
|
437
|
+
messages=messages,
|
438
|
+
prompt_for_code_invoke=prompt_for_code_invoke,
|
439
|
+
function_reference_prefix=function_reference_prefix,
|
440
|
+
function_reference_seperator=function_reference_seperator,
|
441
|
+
)
|
442
|
+
code_obj: PythonCodeToExecute = self.generate_pydantic(
|
443
|
+
response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
|
444
|
+
)
|
445
|
+
return CodeExecutionResult.from_code(
|
446
|
+
code=code_obj.code,
|
447
|
+
config=config,
|
448
|
+
repl_tool=repl_tool,
|
449
|
+
function_signatures=function_signatures,
|
450
|
+
**kwargs,
|
451
|
+
)
|
452
|
+
|
453
|
+
@property
|
454
|
+
def invoke_code_execution(self) -> Callable[..., CodeExecutionResult]:
|
455
|
+
"""Alias for exec method for backward compatibility."""
|
456
|
+
return self.exec
|
457
|
+
|
458
|
+
async def aexec(
|
459
|
+
self,
|
460
|
+
messages: LanguageModelInput,
|
461
|
+
repl_tool: Optional["PythonAstREPLTool"] = None,
|
462
|
+
prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
|
463
|
+
additional_callables: Optional[Callable[..., object] | Sequence[Callable[..., object]]] = None,
|
464
|
+
function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
|
465
|
+
function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
|
466
|
+
config: Optional[RunnableConfig] = None,
|
467
|
+
stop: Optional[list[str]] = None,
|
468
|
+
**kwargs: Any,
|
469
|
+
) -> CodeExecutionResult:
|
470
|
+
function_signatures: list[FunctionSignature] = FunctionSignature.from_callable(additional_callables)
|
471
|
+
messages = augment_prompt_for_toolcall(
|
472
|
+
function_signatures=function_signatures,
|
473
|
+
messages=messages,
|
474
|
+
prompt_for_code_invoke=prompt_for_code_invoke,
|
475
|
+
function_reference_prefix=function_reference_prefix,
|
476
|
+
function_reference_seperator=function_reference_seperator,
|
477
|
+
)
|
478
|
+
code_obj: PythonCodeToExecute = await self.agenerate_pydantic(
|
479
|
+
response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
|
480
|
+
)
|
481
|
+
return await CodeExecutionResult.afrom_code(
|
482
|
+
code=code_obj.code,
|
483
|
+
config=config,
|
484
|
+
repl_tool=repl_tool,
|
485
|
+
function_signatures=function_signatures,
|
486
|
+
**kwargs,
|
487
|
+
)
|
488
|
+
|
489
|
+
@property
|
490
|
+
def ainvoke_code_execution(self):
|
491
|
+
"""Alias for aexec method for backward compatibility."""
|
492
|
+
return self.aexec
|
493
|
+
|
494
|
+
|
495
|
+
class PythonCodeToExecute(BaseModel):
|
496
|
+
code: str = Field(description="Python code to execute")
|
497
|
+
|
498
|
+
def model_post_init(self, context: object) -> None:
|
499
|
+
super().model_post_init(context)
|
500
|
+
|
501
|
+
codes: list[str] = []
|
502
|
+
for match in PYTHON_CODE_PATTERN.finditer(self.code):
|
503
|
+
codes.append(match.group(1))
|
504
|
+
if codes:
|
505
|
+
self.code = "\n".join(codes)
|
506
|
+
|
507
|
+
|
508
|
+
def _with_structured_output(
|
509
|
+
client: BaseChatModel,
|
510
|
+
response_model: Type["PydanticModelT | Partial[PydanticModelT]"],
|
511
|
+
structured_output_kwargs: dict[str, Any],
|
512
|
+
) -> Runnable[LanguageModelInput, dict[object, object] | BaseModel]:
|
513
|
+
return client.with_structured_output(schema=response_model, **structured_output_kwargs) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
514
|
+
|
515
|
+
|
516
|
+
@overload
|
517
|
+
def _get_api_key(api_key: Optional[str], env_key: str, raise_if_none: Literal[True]) -> SecretStr: ...
|
518
|
+
@overload
|
519
|
+
def _get_api_key(api_key: Optional[str], env_key: str, raise_if_none: Literal[False]) -> Optional[SecretStr]: ...
|
520
|
+
def _get_api_key(api_key: Optional[str], env_key: str, raise_if_none: bool) -> Optional[SecretStr]:
|
521
|
+
if api_key is None:
|
522
|
+
api_key_found: SecretStr | None = secret_from_env(env_key, default=None)()
|
523
|
+
if raise_if_none and api_key_found is None:
|
524
|
+
raise ValueError(
|
525
|
+
(
|
526
|
+
f"Did not find API key, please add an environment variable"
|
527
|
+
f" `{env_key}` which contains it, or pass"
|
528
|
+
f" api_key as a named parameter."
|
529
|
+
)
|
530
|
+
)
|
531
|
+
return api_key_found
|
532
|
+
else:
|
533
|
+
return SecretStr(api_key)
|