chatterer 0.1.7__py3-none-any.whl → 0.1.9__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.
@@ -1,371 +1,492 @@
1
- from typing import (
2
- TYPE_CHECKING,
3
- Any,
4
- AsyncIterator,
5
- Iterator,
6
- Optional,
7
- Self,
8
- Type,
9
- TypeAlias,
10
- TypeVar,
11
- cast,
12
- overload,
13
- )
14
-
15
- from langchain_core.language_models.base import LanguageModelInput
16
- from langchain_core.language_models.chat_models import BaseChatModel
17
- from langchain_core.runnables.base import Runnable
18
- from langchain_core.runnables.config import RunnableConfig
19
- from pydantic import BaseModel, Field
20
-
21
- from .messages import AIMessage, BaseMessage, HumanMessage
22
-
23
- if TYPE_CHECKING:
24
- from instructor import Partial
25
-
26
- PydanticModelT = TypeVar("PydanticModelT", bound=BaseModel)
27
- StructuredOutputType: TypeAlias = dict[object, object] | BaseModel
28
-
29
- DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION = "Just describe all the details you see in the image in few sentences."
30
-
31
-
32
- class Chatterer(BaseModel):
33
- """Language model for generating text from a given input."""
34
-
35
- client: BaseChatModel
36
- structured_output_kwargs: dict[str, Any] = Field(default_factory=dict)
37
-
38
- @overload
39
- def __call__(
40
- self,
41
- messages: LanguageModelInput,
42
- response_model: Type[PydanticModelT],
43
- config: Optional[RunnableConfig] = None,
44
- stop: Optional[list[str]] = None,
45
- **kwargs: Any,
46
- ) -> PydanticModelT: ...
47
-
48
- @overload
49
- def __call__(
50
- self,
51
- messages: LanguageModelInput,
52
- response_model: None = None,
53
- config: Optional[RunnableConfig] = None,
54
- stop: Optional[list[str]] = None,
55
- **kwargs: Any,
56
- ) -> str: ...
57
-
58
- def __call__(
59
- self,
60
- messages: LanguageModelInput,
61
- response_model: Optional[Type[PydanticModelT]] = None,
62
- config: Optional[RunnableConfig] = None,
63
- stop: Optional[list[str]] = None,
64
- **kwargs: Any,
65
- ) -> str | PydanticModelT:
66
- if response_model:
67
- return self.generate_pydantic(response_model, messages, config, stop, **kwargs)
68
- return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
69
-
70
- @classmethod
71
- def openai(
72
- cls,
73
- model: str = "gpt-4o-mini",
74
- structured_output_kwargs: Optional[dict[str, Any]] = {"strict": True},
75
- ) -> Self:
76
- from langchain_openai import ChatOpenAI
77
-
78
- return cls(client=ChatOpenAI(model=model), structured_output_kwargs=structured_output_kwargs or {})
79
-
80
- @classmethod
81
- def anthropic(
82
- cls,
83
- model_name: str = "claude-3-7-sonnet-20250219",
84
- structured_output_kwargs: Optional[dict[str, Any]] = None,
85
- ) -> Self:
86
- from langchain_anthropic import ChatAnthropic
87
-
88
- return cls(
89
- client=ChatAnthropic(model_name=model_name, timeout=None, stop=None),
90
- structured_output_kwargs=structured_output_kwargs or {},
91
- )
92
-
93
- @classmethod
94
- def google(
95
- cls,
96
- model: str = "gemini-2.0-flash",
97
- structured_output_kwargs: Optional[dict[str, Any]] = None,
98
- ) -> Self:
99
- from langchain_google_genai import ChatGoogleGenerativeAI
100
-
101
- return cls(
102
- client=ChatGoogleGenerativeAI(model=model),
103
- structured_output_kwargs=structured_output_kwargs or {},
104
- )
105
-
106
- @classmethod
107
- def ollama(
108
- cls,
109
- model: str = "deepseek-r1:1.5b",
110
- structured_output_kwargs: Optional[dict[str, Any]] = None,
111
- ) -> Self:
112
- from langchain_ollama import ChatOllama
113
-
114
- return cls(
115
- client=ChatOllama(model=model),
116
- structured_output_kwargs=structured_output_kwargs or {},
117
- )
118
-
119
- def generate(
120
- self,
121
- messages: LanguageModelInput,
122
- config: Optional[RunnableConfig] = None,
123
- stop: Optional[list[str]] = None,
124
- **kwargs: Any,
125
- ) -> str:
126
- return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
127
-
128
- async def agenerate(
129
- self,
130
- messages: LanguageModelInput,
131
- config: Optional[RunnableConfig] = None,
132
- stop: Optional[list[str]] = None,
133
- **kwargs: Any,
134
- ) -> str:
135
- return (await self.client.ainvoke(input=messages, config=config, stop=stop, **kwargs)).text()
136
-
137
- def generate_stream(
138
- self,
139
- messages: LanguageModelInput,
140
- config: Optional[RunnableConfig] = None,
141
- stop: Optional[list[str]] = None,
142
- **kwargs: Any,
143
- ) -> Iterator[str]:
144
- for chunk in self.client.stream(input=messages, config=config, stop=stop, **kwargs):
145
- yield chunk.text()
146
-
147
- async def agenerate_stream(
148
- self,
149
- messages: LanguageModelInput,
150
- config: Optional[RunnableConfig] = None,
151
- stop: Optional[list[str]] = None,
152
- **kwargs: Any,
153
- ) -> AsyncIterator[str]:
154
- async for chunk in self.client.astream(input=messages, config=config, stop=stop, **kwargs):
155
- yield chunk.text()
156
-
157
- def generate_pydantic(
158
- self,
159
- response_model: Type[PydanticModelT],
160
- messages: LanguageModelInput,
161
- config: Optional[RunnableConfig] = None,
162
- stop: Optional[list[str]] = None,
163
- **kwargs: Any,
164
- ) -> PydanticModelT:
165
- result: StructuredOutputType = with_structured_output(
166
- client=self.client,
167
- response_model=response_model,
168
- structured_output_kwargs=self.structured_output_kwargs,
169
- ).invoke(input=messages, config=config, stop=stop, **kwargs)
170
- if isinstance(result, response_model):
171
- return result
172
- else:
173
- return response_model.model_validate(result)
174
-
175
- async def agenerate_pydantic(
176
- self,
177
- response_model: Type[PydanticModelT],
178
- messages: LanguageModelInput,
179
- config: Optional[RunnableConfig] = None,
180
- stop: Optional[list[str]] = None,
181
- **kwargs: Any,
182
- ) -> PydanticModelT:
183
- result: StructuredOutputType = await with_structured_output(
184
- client=self.client,
185
- response_model=response_model,
186
- structured_output_kwargs=self.structured_output_kwargs,
187
- ).ainvoke(input=messages, config=config, stop=stop, **kwargs)
188
- if isinstance(result, response_model):
189
- return result
190
- else:
191
- return response_model.model_validate(result)
192
-
193
- def generate_pydantic_stream(
194
- self,
195
- response_model: Type[PydanticModelT],
196
- messages: LanguageModelInput,
197
- config: Optional[RunnableConfig] = None,
198
- stop: Optional[list[str]] = None,
199
- **kwargs: Any,
200
- ) -> Iterator[PydanticModelT]:
201
- try:
202
- import instructor
203
- except ImportError:
204
- raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
205
-
206
- partial_response_model = instructor.Partial[response_model]
207
- for chunk in with_structured_output(
208
- client=self.client,
209
- response_model=partial_response_model,
210
- structured_output_kwargs=self.structured_output_kwargs,
211
- ).stream(input=messages, config=config, stop=stop, **kwargs):
212
- yield response_model.model_validate(chunk)
213
-
214
- async def agenerate_pydantic_stream(
215
- self,
216
- response_model: Type[PydanticModelT],
217
- messages: LanguageModelInput,
218
- config: Optional[RunnableConfig] = None,
219
- stop: Optional[list[str]] = None,
220
- **kwargs: Any,
221
- ) -> AsyncIterator[PydanticModelT]:
222
- try:
223
- import instructor
224
- except ImportError:
225
- raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
226
-
227
- partial_response_model = instructor.Partial[response_model]
228
- async for chunk in with_structured_output(
229
- client=self.client,
230
- response_model=partial_response_model,
231
- structured_output_kwargs=self.structured_output_kwargs,
232
- ).astream(input=messages, config=config, stop=stop, **kwargs):
233
- yield response_model.model_validate(chunk)
234
-
235
- def describe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
236
- """
237
- Create a detailed description of an image using the Vision Language Model.
238
- - image_url: Image URL to describe
239
- """
240
- return self.generate([
241
- HumanMessage(
242
- content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
243
- )
244
- ])
245
-
246
- async def adescribe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
247
- """
248
- Create a detailed description of an image using the Vision Language Model asynchronously.
249
- - image_url: Image URL to describe
250
- """
251
- return await self.agenerate([
252
- HumanMessage(
253
- content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
254
- )
255
- ])
256
-
257
- @staticmethod
258
- def get_num_tokens_from_message(message: BaseMessage) -> Optional[tuple[int, int]]:
259
- try:
260
- if isinstance(message, AIMessage) and (usage_metadata := message.usage_metadata):
261
- input_tokens = int(usage_metadata["input_tokens"])
262
- output_tokens = int(usage_metadata["output_tokens"])
263
- else:
264
- # Dynamic extraction for unknown structures
265
- input_tokens: Optional[int] = None
266
- output_tokens: Optional[int] = None
267
-
268
- def _find_tokens(obj: object) -> None:
269
- nonlocal input_tokens, output_tokens
270
- if isinstance(obj, dict):
271
- for key, value in cast(dict[object, object], obj).items():
272
- if isinstance(value, int):
273
- if "input" in str(key) or "prompt" in str(key):
274
- input_tokens = value
275
- elif "output" in str(key) or "completion" in str(key):
276
- output_tokens = value
277
- else:
278
- _find_tokens(value)
279
- elif isinstance(obj, list):
280
- for item in cast(list[object], obj):
281
- _find_tokens(item)
282
-
283
- _find_tokens(message.model_dump())
284
-
285
- if input_tokens is None or output_tokens is None:
286
- return None
287
- return input_tokens, output_tokens
288
- except Exception:
289
- return None
290
-
291
-
292
- def with_structured_output(
293
- client: BaseChatModel,
294
- response_model: Type["PydanticModelT | Partial[PydanticModelT]"],
295
- structured_output_kwargs: dict[str, Any],
296
- ) -> Runnable[LanguageModelInput, dict[object, object] | BaseModel]:
297
- return client.with_structured_output(schema=response_model, **structured_output_kwargs) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
298
-
299
-
300
- if __name__ == "__main__":
301
- import asyncio
302
-
303
- # 테스트용 Pydantic 모델 정의
304
- class Propositions(BaseModel):
305
- proposition_topic: str
306
- proposition_content: str
307
-
308
- chatterer = Chatterer.openai()
309
- prompt = "What is the meaning of life?"
310
-
311
- # === Synchronous Tests ===
312
-
313
- # generate
314
- print("=== Synchronous generate ===")
315
- result_sync = chatterer(prompt)
316
- print("Result (generate):", result_sync)
317
-
318
- # generate_stream
319
- print("\n=== Synchronous generate_stream ===")
320
- for i, chunk in enumerate(chatterer.generate_stream(prompt)):
321
- print(f"Chunk {i}:", chunk)
322
-
323
- # generate_pydantic
324
- print("\n=== Synchronous generate_pydantic ===")
325
- result_pydantic = chatterer(prompt, Propositions)
326
- print("Result (generate_pydantic):", result_pydantic)
327
-
328
- # generate_pydantic_stream
329
- print("\n=== Synchronous generate_pydantic_stream ===")
330
- for i, chunk in enumerate(chatterer.generate_pydantic_stream(Propositions, prompt)):
331
- print(f"Pydantic Chunk {i}:", chunk)
332
-
333
- # === Asynchronous Tests ===
334
-
335
- # Async helper function to enumerate async iterator
336
- async def async_enumerate(aiter: AsyncIterator[Any], start: int = 0) -> AsyncIterator[tuple[int, Any]]:
337
- i = start
338
- async for item in aiter:
339
- yield i, item
340
- i += 1
341
-
342
- async def run_async_tests():
343
- # 6. agenerate
344
- print("\n=== Asynchronous agenerate ===")
345
- result_async = await chatterer.agenerate(prompt)
346
- print("Result (agenerate):", result_async)
347
-
348
- # 7. agenerate_stream
349
- print("\n=== Asynchronous agenerate_stream ===")
350
- async for i, chunk in async_enumerate(chatterer.agenerate_stream(prompt)):
351
- print(f"Async Chunk {i}:", chunk)
352
-
353
- # 8. agenerate_pydantic
354
- print("\n=== Asynchronous agenerate_pydantic ===")
355
- try:
356
- result_async_pydantic = await chatterer.agenerate_pydantic(Propositions, prompt)
357
- print("Result (agenerate_pydantic):", result_async_pydantic)
358
- except Exception as e:
359
- print("Error in agenerate_pydantic:", e)
360
-
361
- # 9. agenerate_pydantic_stream
362
- print("\n=== Asynchronous agenerate_pydantic_stream ===")
363
- try:
364
- i = 0
365
- async for chunk in chatterer.agenerate_pydantic_stream(Propositions, prompt):
366
- print(f"Async Pydantic Chunk {i}:", chunk)
367
- i += 1
368
- except Exception as e:
369
- print("Error in agenerate_pydantic_stream:", e)
370
-
371
- asyncio.run(run_async_tests())
1
+ from typing import (
2
+ TYPE_CHECKING,
3
+ Any,
4
+ AsyncIterator,
5
+ Callable,
6
+ Iterable,
7
+ Iterator,
8
+ Optional,
9
+ Self,
10
+ Sequence,
11
+ Type,
12
+ TypeAlias,
13
+ TypeVar,
14
+ cast,
15
+ overload,
16
+ )
17
+
18
+ from langchain_core.language_models.base import LanguageModelInput
19
+ from langchain_core.language_models.chat_models import BaseChatModel
20
+ from langchain_core.runnables.base import Runnable
21
+ from langchain_core.runnables.config import RunnableConfig
22
+ from pydantic import BaseModel, Field
23
+
24
+ from .messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
25
+ from .utils.code_agent import CodeExecutionResult, FunctionSignature
26
+
27
+ if TYPE_CHECKING:
28
+ from instructor import Partial
29
+ from langchain_experimental.tools.python.tool import PythonAstREPLTool
30
+
31
+ PydanticModelT = TypeVar("PydanticModelT", bound=BaseModel)
32
+ StructuredOutputType: TypeAlias = dict[object, object] | BaseModel
33
+
34
+ DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION = "Provide a detailed description of all visible elements in the image, summarizing key details in a few clear sentences."
35
+ DEFAULT_CODE_GENERATION_PROMPT = (
36
+ "You are utilizing a Python code execution tool now.\n"
37
+ "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"
38
+ "Since your context window is highly limited, type `pass` if no code execution is needed.\n"
39
+ "\n"
40
+ "To optimize tool efficiency, follow these guidelines:\n"
41
+ "- Write concise, efficient code that directly serves the intended purpose.\n"
42
+ "- Avoid unnecessary operations (e.g., excessive loops, recursion, or heavy computations).\n"
43
+ "- Handle potential errors gracefully (e.g., using try-except blocks).\n"
44
+ "- Prevent excessive output by limiting print statements to essential information only (e.g., avoid printing large datasets).\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
+
59
+ class Chatterer(BaseModel):
60
+ """Language model for generating text from a given input."""
61
+
62
+ client: BaseChatModel
63
+ structured_output_kwargs: dict[str, Any] = Field(default_factory=dict)
64
+
65
+ @overload
66
+ def __call__(
67
+ self,
68
+ messages: LanguageModelInput,
69
+ response_model: Type[PydanticModelT],
70
+ config: Optional[RunnableConfig] = None,
71
+ stop: Optional[list[str]] = None,
72
+ **kwargs: Any,
73
+ ) -> PydanticModelT: ...
74
+
75
+ @overload
76
+ def __call__(
77
+ self,
78
+ messages: LanguageModelInput,
79
+ response_model: None = None,
80
+ config: Optional[RunnableConfig] = None,
81
+ stop: Optional[list[str]] = None,
82
+ **kwargs: Any,
83
+ ) -> str: ...
84
+
85
+ def __call__(
86
+ self,
87
+ messages: LanguageModelInput,
88
+ response_model: Optional[Type[PydanticModelT]] = None,
89
+ config: Optional[RunnableConfig] = None,
90
+ stop: Optional[list[str]] = None,
91
+ **kwargs: Any,
92
+ ) -> str | PydanticModelT:
93
+ if response_model:
94
+ return self.generate_pydantic(response_model, messages, config, stop, **kwargs)
95
+ return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
96
+
97
+ @classmethod
98
+ def openai(
99
+ cls,
100
+ model: str = "gpt-4o-mini",
101
+ structured_output_kwargs: Optional[dict[str, Any]] = {"strict": True},
102
+ ) -> Self:
103
+ from langchain_openai import ChatOpenAI
104
+
105
+ return cls(client=ChatOpenAI(model=model), structured_output_kwargs=structured_output_kwargs or {})
106
+
107
+ @classmethod
108
+ def anthropic(
109
+ cls,
110
+ model_name: str = "claude-3-7-sonnet-20250219",
111
+ structured_output_kwargs: Optional[dict[str, Any]] = None,
112
+ ) -> Self:
113
+ from langchain_anthropic import ChatAnthropic
114
+
115
+ return cls(
116
+ client=ChatAnthropic(model_name=model_name, timeout=None, stop=None),
117
+ structured_output_kwargs=structured_output_kwargs or {},
118
+ )
119
+
120
+ @classmethod
121
+ def google(
122
+ cls,
123
+ model: str = "gemini-2.0-flash",
124
+ structured_output_kwargs: Optional[dict[str, Any]] = None,
125
+ ) -> Self:
126
+ from langchain_google_genai import ChatGoogleGenerativeAI
127
+
128
+ return cls(
129
+ client=ChatGoogleGenerativeAI(model=model),
130
+ structured_output_kwargs=structured_output_kwargs or {},
131
+ )
132
+
133
+ @classmethod
134
+ def ollama(
135
+ cls,
136
+ model: str = "deepseek-r1:1.5b",
137
+ structured_output_kwargs: Optional[dict[str, Any]] = None,
138
+ ) -> Self:
139
+ from langchain_ollama import ChatOllama
140
+
141
+ return cls(
142
+ client=ChatOllama(model=model),
143
+ structured_output_kwargs=structured_output_kwargs or {},
144
+ )
145
+
146
+ def generate(
147
+ self,
148
+ messages: LanguageModelInput,
149
+ config: Optional[RunnableConfig] = None,
150
+ stop: Optional[list[str]] = None,
151
+ **kwargs: Any,
152
+ ) -> str:
153
+ return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
154
+
155
+ async def agenerate(
156
+ self,
157
+ messages: LanguageModelInput,
158
+ config: Optional[RunnableConfig] = None,
159
+ stop: Optional[list[str]] = None,
160
+ **kwargs: Any,
161
+ ) -> str:
162
+ return (await self.client.ainvoke(input=messages, config=config, stop=stop, **kwargs)).text()
163
+
164
+ def generate_stream(
165
+ self,
166
+ messages: LanguageModelInput,
167
+ config: Optional[RunnableConfig] = None,
168
+ stop: Optional[list[str]] = None,
169
+ **kwargs: Any,
170
+ ) -> Iterator[str]:
171
+ for chunk in self.client.stream(input=messages, config=config, stop=stop, **kwargs):
172
+ yield chunk.text()
173
+
174
+ async def agenerate_stream(
175
+ self,
176
+ messages: LanguageModelInput,
177
+ config: Optional[RunnableConfig] = None,
178
+ stop: Optional[list[str]] = None,
179
+ **kwargs: Any,
180
+ ) -> AsyncIterator[str]:
181
+ async for chunk in self.client.astream(input=messages, config=config, stop=stop, **kwargs):
182
+ yield chunk.text()
183
+
184
+ def generate_pydantic(
185
+ self,
186
+ response_model: Type[PydanticModelT],
187
+ messages: LanguageModelInput,
188
+ config: Optional[RunnableConfig] = None,
189
+ stop: Optional[list[str]] = None,
190
+ **kwargs: Any,
191
+ ) -> PydanticModelT:
192
+ result: StructuredOutputType = with_structured_output(
193
+ client=self.client,
194
+ response_model=response_model,
195
+ structured_output_kwargs=self.structured_output_kwargs,
196
+ ).invoke(input=messages, config=config, stop=stop, **kwargs)
197
+ if isinstance(result, response_model):
198
+ return result
199
+ else:
200
+ return response_model.model_validate(result)
201
+
202
+ async def agenerate_pydantic(
203
+ self,
204
+ response_model: Type[PydanticModelT],
205
+ messages: LanguageModelInput,
206
+ config: Optional[RunnableConfig] = None,
207
+ stop: Optional[list[str]] = None,
208
+ **kwargs: Any,
209
+ ) -> PydanticModelT:
210
+ result: StructuredOutputType = await with_structured_output(
211
+ client=self.client,
212
+ response_model=response_model,
213
+ structured_output_kwargs=self.structured_output_kwargs,
214
+ ).ainvoke(input=messages, config=config, stop=stop, **kwargs)
215
+ if isinstance(result, response_model):
216
+ return result
217
+ else:
218
+ return response_model.model_validate(result)
219
+
220
+ def generate_pydantic_stream(
221
+ self,
222
+ response_model: Type[PydanticModelT],
223
+ messages: LanguageModelInput,
224
+ config: Optional[RunnableConfig] = None,
225
+ stop: Optional[list[str]] = None,
226
+ **kwargs: Any,
227
+ ) -> Iterator[PydanticModelT]:
228
+ try:
229
+ import instructor
230
+ except ImportError:
231
+ raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
232
+
233
+ partial_response_model = instructor.Partial[response_model]
234
+ for chunk in with_structured_output(
235
+ client=self.client,
236
+ response_model=partial_response_model,
237
+ structured_output_kwargs=self.structured_output_kwargs,
238
+ ).stream(input=messages, config=config, stop=stop, **kwargs):
239
+ yield response_model.model_validate(chunk)
240
+
241
+ async def agenerate_pydantic_stream(
242
+ self,
243
+ response_model: Type[PydanticModelT],
244
+ messages: LanguageModelInput,
245
+ config: Optional[RunnableConfig] = None,
246
+ stop: Optional[list[str]] = None,
247
+ **kwargs: Any,
248
+ ) -> AsyncIterator[PydanticModelT]:
249
+ try:
250
+ import instructor
251
+ except ImportError:
252
+ raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
253
+
254
+ partial_response_model = instructor.Partial[response_model]
255
+ async for chunk in with_structured_output(
256
+ client=self.client,
257
+ response_model=partial_response_model,
258
+ structured_output_kwargs=self.structured_output_kwargs,
259
+ ).astream(input=messages, config=config, stop=stop, **kwargs):
260
+ yield response_model.model_validate(chunk)
261
+
262
+ def describe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
263
+ """
264
+ Create a detailed description of an image using the Vision Language Model.
265
+ - image_url: Image URL to describe
266
+ """
267
+ return self.generate([
268
+ HumanMessage(
269
+ content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
270
+ )
271
+ ])
272
+
273
+ async def adescribe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
274
+ """
275
+ Create a detailed description of an image using the Vision Language Model asynchronously.
276
+ - image_url: Image URL to describe
277
+ """
278
+ return await self.agenerate([
279
+ HumanMessage(
280
+ content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
281
+ )
282
+ ])
283
+
284
+ @staticmethod
285
+ def get_num_tokens_from_message(message: BaseMessage) -> Optional[tuple[int, int]]:
286
+ try:
287
+ if isinstance(message, AIMessage) and (usage_metadata := message.usage_metadata):
288
+ input_tokens = int(usage_metadata["input_tokens"])
289
+ output_tokens = int(usage_metadata["output_tokens"])
290
+ else:
291
+ # Dynamic extraction for unknown structures
292
+ input_tokens: Optional[int] = None
293
+ output_tokens: Optional[int] = None
294
+
295
+ def _find_tokens(obj: object) -> None:
296
+ nonlocal input_tokens, output_tokens
297
+ if isinstance(obj, dict):
298
+ for key, value in cast(dict[object, object], obj).items():
299
+ if isinstance(value, int):
300
+ if "input" in str(key) or "prompt" in str(key):
301
+ input_tokens = value
302
+ elif "output" in str(key) or "completion" in str(key):
303
+ output_tokens = value
304
+ else:
305
+ _find_tokens(value)
306
+ elif isinstance(obj, list):
307
+ for item in cast(list[object], obj):
308
+ _find_tokens(item)
309
+
310
+ _find_tokens(message.model_dump())
311
+
312
+ if input_tokens is None or output_tokens is None:
313
+ return None
314
+ return input_tokens, output_tokens
315
+ except Exception:
316
+ return None
317
+
318
+ def invoke_code_execution(
319
+ self,
320
+ messages: LanguageModelInput,
321
+ repl_tool: Optional["PythonAstREPLTool"] = None,
322
+ prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
323
+ additional_callables: Optional[Callable[..., object] | Sequence[Callable[..., object]]] = None,
324
+ function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
325
+ function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
326
+ config: Optional[RunnableConfig] = None,
327
+ stop: Optional[list[str]] = None,
328
+ **kwargs: Any,
329
+ ) -> CodeExecutionResult:
330
+ function_signatures: Optional[list[FunctionSignature]] = None
331
+ if additional_callables:
332
+ if not isinstance(additional_callables, Iterable):
333
+ additional_callables = (additional_callables,)
334
+ function_signatures = FunctionSignature.from_callables(additional_callables)
335
+ messages = _add_message_last(
336
+ messages=messages,
337
+ prompt_to_add=FunctionSignature.as_prompt(
338
+ function_signatures, function_reference_prefix, function_reference_seperator
339
+ ),
340
+ )
341
+ if prompt_for_code_invoke:
342
+ messages = _add_message_last(messages=messages, prompt_to_add=prompt_for_code_invoke)
343
+ code_obj: PythonCodeToExecute = self.generate_pydantic(
344
+ response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
345
+ )
346
+ return CodeExecutionResult.from_code(
347
+ code=code_obj.code,
348
+ config=config,
349
+ repl_tool=repl_tool,
350
+ function_signatures=function_signatures,
351
+ **kwargs,
352
+ )
353
+
354
+ async def ainvoke_code_execution(
355
+ self,
356
+ messages: LanguageModelInput,
357
+ repl_tool: Optional["PythonAstREPLTool"] = None,
358
+ prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
359
+ additional_callables: Optional[Callable[..., object] | Sequence[Callable[..., object]]] = None,
360
+ function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
361
+ function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
362
+ config: Optional[RunnableConfig] = None,
363
+ stop: Optional[list[str]] = None,
364
+ **kwargs: Any,
365
+ ) -> CodeExecutionResult:
366
+ function_signatures: Optional[list[FunctionSignature]] = None
367
+ if additional_callables:
368
+ if not isinstance(additional_callables, Iterable):
369
+ additional_callables = (additional_callables,)
370
+ function_signatures = FunctionSignature.from_callables(additional_callables)
371
+ messages = _add_message_last(
372
+ messages=messages,
373
+ prompt_to_add=FunctionSignature.as_prompt(
374
+ function_signatures, function_reference_prefix, function_reference_seperator
375
+ ),
376
+ )
377
+ if prompt_for_code_invoke:
378
+ messages = _add_message_last(messages=messages, prompt_to_add=prompt_for_code_invoke)
379
+ code_obj: PythonCodeToExecute = await self.agenerate_pydantic(
380
+ response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
381
+ )
382
+ return await CodeExecutionResult.afrom_code(
383
+ code=code_obj.code,
384
+ config=config,
385
+ repl_tool=repl_tool,
386
+ function_signatures=function_signatures,
387
+ **kwargs,
388
+ )
389
+
390
+
391
+ class PythonCodeToExecute(BaseModel):
392
+ code: str = Field(description="Python code to execute")
393
+
394
+
395
+ def with_structured_output(
396
+ client: BaseChatModel,
397
+ response_model: Type["PydanticModelT | Partial[PydanticModelT]"],
398
+ structured_output_kwargs: dict[str, Any],
399
+ ) -> Runnable[LanguageModelInput, dict[object, object] | BaseModel]:
400
+ return client.with_structured_output(schema=response_model, **structured_output_kwargs) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
401
+
402
+
403
+ def _add_message_last(messages: LanguageModelInput, prompt_to_add: str) -> LanguageModelInput:
404
+ if isinstance(messages, str):
405
+ messages += f"\n{prompt_to_add}"
406
+ elif isinstance(messages, Sequence):
407
+ messages = list(messages)
408
+ messages.append(SystemMessage(content=prompt_to_add))
409
+ else:
410
+ messages = messages.to_messages()
411
+ messages.append(SystemMessage(content=prompt_to_add))
412
+ return messages
413
+
414
+
415
+ # def _add_message_first(messages: LanguageModelInput, prompt_to_add: str) -> LanguageModelInput:
416
+ # if isinstance(messages, str):
417
+ # messages = f"{prompt_to_add}\n{messages}"
418
+ # elif isinstance(messages, Sequence):
419
+ # messages = list(messages)
420
+ # messages.insert(0, SystemMessage(content=prompt_to_add))
421
+ # else:
422
+ # messages = messages.to_messages()
423
+ # messages.insert(0, SystemMessage(content=prompt_to_add))
424
+ # return messages
425
+
426
+
427
+ def chatbot_example(chatterer: Chatterer = Chatterer.openai()) -> None:
428
+ # Define the CodeExecutionDecision class using Pydantic
429
+
430
+ from rich.console import Console
431
+ from rich.prompt import Prompt
432
+
433
+ class CodeExecutionDecision(BaseModel):
434
+ is_code_execution_needed: bool = Field(
435
+ description="Whether Python tool calling is needed to answer user query."
436
+ )
437
+
438
+ # Initialize Rich console
439
+ console = Console()
440
+
441
+ # Initialize conversation context
442
+ context: list[BaseMessage] = [SystemMessage("You are an AI that can answer questions and execute Python code.")]
443
+
444
+ # Display welcome message
445
+ console.print("[bold blue]Welcome to the Rich-based chatbot![/bold blue]")
446
+ console.print("Type 'quit' or 'exit' to end the conversation.")
447
+
448
+ while True:
449
+ # Get user input
450
+ user_input = Prompt.ask("[bold green]You[/bold green]")
451
+ if user_input.lower() in ["quit", "exit"]:
452
+ console.print("[bold blue]Goodbye![/bold blue]")
453
+ break
454
+
455
+ # Add user message to context
456
+ context.append(HumanMessage(content=user_input))
457
+
458
+ # Determine if code execution is needed
459
+ decision = chatterer.generate_pydantic(
460
+ response_model=CodeExecutionDecision, # Use response_model instead of pydantic_model
461
+ messages=context,
462
+ )
463
+
464
+ if decision.is_code_execution_needed:
465
+ # Execute code if needed
466
+ code_result = chatterer.invoke_code_execution(messages=context)
467
+ if code_result.code.strip() == "pass":
468
+ new_message = None
469
+ else:
470
+ new_message = SystemMessage(
471
+ content=f"Executed code:\n```python\n{code_result.code}\n```\nOutput:\n{code_result.output}"
472
+ )
473
+ console.print("[bold yellow]Executed code:[/bold yellow]")
474
+ console.print(f"[code]{code_result.code}[/code]")
475
+ console.print("[bold yellow]Output:[/bold yellow]")
476
+ console.print(code_result.output)
477
+ else:
478
+ # No code execution required
479
+ new_message = None
480
+
481
+ # Add system message to context
482
+ if new_message:
483
+ context.append(new_message)
484
+
485
+ # Generate and display chatbot response
486
+ response = chatterer.generate(messages=context) # Use generate instead of generate_response
487
+ context.append(AIMessage(content=response))
488
+ console.print(f"[bold blue]Chatbot:[/bold blue] {response}")
489
+
490
+
491
+ if __name__ == "__main__":
492
+ chatbot_example()