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.
- chatterer/__init__.py +55 -39
- chatterer/language_model.py +492 -371
- chatterer/messages.py +9 -8
- chatterer/strategies/__init__.py +13 -13
- chatterer/strategies/atom_of_thoughts.py +975 -975
- chatterer/strategies/base.py +14 -14
- chatterer/tools/__init__.py +25 -17
- 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_to_text.py +463 -466
- chatterer/tools/webpage_to_markdown/__init__.py +4 -4
- chatterer/tools/webpage_to_markdown/playwright_bot.py +649 -649
- chatterer/tools/webpage_to_markdown/utils.py +329 -329
- chatterer/utils/__init__.py +15 -0
- chatterer/utils/code_agent.py +134 -0
- chatterer/utils/image.py +288 -284
- {chatterer-0.1.7.dist-info → chatterer-0.1.9.dist-info}/METADATA +169 -166
- chatterer-0.1.9.dist-info/RECORD +26 -0
- {chatterer-0.1.7.dist-info → chatterer-0.1.9.dist-info}/WHEEL +1 -1
- chatterer-0.1.7.dist-info/RECORD +0 -24
- {chatterer-0.1.7.dist-info → chatterer-0.1.9.dist-info}/top_level.txt +0 -0
chatterer/language_model.py
CHANGED
@@ -1,371 +1,492 @@
|
|
1
|
-
from typing import (
|
2
|
-
TYPE_CHECKING,
|
3
|
-
Any,
|
4
|
-
AsyncIterator,
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
from langchain_core.
|
19
|
-
from
|
20
|
-
|
21
|
-
from .
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
) ->
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
**kwargs:
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
messages
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
if
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
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()
|