chatterer 0.1.10__py3-none-any.whl → 0.1.12__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,580 +1,577 @@
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, get_default_repl_tool
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
- function_signatures: Optional[FunctionSignature | Iterable[FunctionSignature]] = 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
- if not function_signatures:
331
- function_signatures = []
332
- elif isinstance(function_signatures, FunctionSignature):
333
- function_signatures = [function_signatures]
334
- messages = augment_prompt_for_toolcall(
335
- function_signatures=function_signatures,
336
- messages=messages,
337
- prompt_for_code_invoke=prompt_for_code_invoke,
338
- function_reference_prefix=function_reference_prefix,
339
- function_reference_seperator=function_reference_seperator,
340
- )
341
- code_obj: PythonCodeToExecute = self.generate_pydantic(
342
- response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
343
- )
344
- return CodeExecutionResult.from_code(
345
- code=code_obj.code,
346
- config=config,
347
- repl_tool=repl_tool,
348
- function_signatures=function_signatures,
349
- **kwargs,
350
- )
351
-
352
- async def ainvoke_code_execution(
353
- self,
354
- messages: LanguageModelInput,
355
- repl_tool: Optional["PythonAstREPLTool"] = None,
356
- prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
357
- additional_callables: Optional[Callable[..., object] | Sequence[Callable[..., object]]] = None,
358
- function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
359
- function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
360
- config: Optional[RunnableConfig] = None,
361
- stop: Optional[list[str]] = None,
362
- **kwargs: Any,
363
- ) -> CodeExecutionResult:
364
- function_signatures: list[FunctionSignature] = FunctionSignature.from_callable(additional_callables)
365
- messages = augment_prompt_for_toolcall(
366
- function_signatures=function_signatures,
367
- messages=messages,
368
- prompt_for_code_invoke=prompt_for_code_invoke,
369
- function_reference_prefix=function_reference_prefix,
370
- function_reference_seperator=function_reference_seperator,
371
- )
372
- code_obj: PythonCodeToExecute = await self.agenerate_pydantic(
373
- response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
374
- )
375
- return await CodeExecutionResult.afrom_code(
376
- code=code_obj.code,
377
- config=config,
378
- repl_tool=repl_tool,
379
- function_signatures=function_signatures,
380
- **kwargs,
381
- )
382
-
383
-
384
- class PythonCodeToExecute(BaseModel):
385
- code: str = Field(description="Python code to execute")
386
-
387
-
388
- def _with_structured_output(
389
- client: BaseChatModel,
390
- response_model: Type["PydanticModelT | Partial[PydanticModelT]"],
391
- structured_output_kwargs: dict[str, Any],
392
- ) -> Runnable[LanguageModelInput, dict[object, object] | BaseModel]:
393
- return client.with_structured_output(schema=response_model, **structured_output_kwargs) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
394
-
395
-
396
- def _add_message_last(messages: LanguageModelInput, prompt_to_add: str) -> LanguageModelInput:
397
- if isinstance(messages, str):
398
- messages += f"\n{prompt_to_add}"
399
- elif isinstance(messages, Sequence):
400
- messages = list(messages)
401
- messages.append(SystemMessage(content=prompt_to_add))
402
- else:
403
- messages = messages.to_messages()
404
- messages.append(SystemMessage(content=prompt_to_add))
405
- return messages
406
-
407
-
408
- # def _add_message_first(messages: LanguageModelInput, prompt_to_add: str) -> LanguageModelInput:
409
- # if isinstance(messages, str):
410
- # messages = f"{prompt_to_add}\n{messages}"
411
- # elif isinstance(messages, Sequence):
412
- # messages = list(messages)
413
- # messages.insert(0, SystemMessage(content=prompt_to_add))
414
- # else:
415
- # messages = messages.to_messages()
416
- # messages.insert(0, SystemMessage(content=prompt_to_add))
417
- # return messages
418
-
419
-
420
- def augment_prompt_for_toolcall(
421
- function_signatures: Iterable[FunctionSignature],
422
- messages: LanguageModelInput,
423
- prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
424
- function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
425
- function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
426
- ) -> LanguageModelInput:
427
- if function_signatures:
428
- messages = _add_message_last(
429
- messages=messages,
430
- prompt_to_add=FunctionSignature.as_prompt(
431
- function_signatures, function_reference_prefix, function_reference_seperator
432
- ),
433
- )
434
- if prompt_for_code_invoke:
435
- messages = _add_message_last(messages=messages, prompt_to_add=prompt_for_code_invoke)
436
- return messages
437
-
438
-
439
- def interactive_shell(
440
- chatterer: Chatterer = Chatterer.openai(),
441
- system_instruction: BaseMessage | Iterable[BaseMessage] = ([
442
- SystemMessage("You are an AI that can answer questions and execute Python code."),
443
- ]),
444
- repl_tool: Optional["PythonAstREPLTool"] = None,
445
- prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
446
- additional_callables: Optional[Callable[..., object] | Sequence[Callable[..., object]]] = None,
447
- function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
448
- function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
449
- config: Optional[RunnableConfig] = None,
450
- stop: Optional[list[str]] = None,
451
- **kwargs: Any,
452
- ) -> None:
453
- # Define the CodeExecutionDecision class using Pydantic
454
-
455
- from rich.console import Console
456
- from rich.prompt import Prompt
457
-
458
- class IsCodeExecutionNeeded(BaseModel):
459
- is_code_execution_needed: bool = Field(
460
- description="Whether Python tool calling is needed to answer user query."
461
- )
462
-
463
- class IsFurtherCodeExecutionNeeded(BaseModel):
464
- review_on_code_execution: str = Field(description="Review on the code execution.")
465
- next_action: str = Field(description="Next action to take.")
466
- is_further_code_execution_needed: bool = Field(
467
- description="Whether further Python tool calling is needed to answer user query."
468
- )
469
-
470
- # Get default REPL tool if not provided.
471
- # This tool namespace is persistent across multiple code executions.
472
- if repl_tool is None:
473
- repl_tool = get_default_repl_tool()
474
-
475
- function_signatures: list[FunctionSignature] = FunctionSignature.from_callable(additional_callables)
476
-
477
- # Initialize Rich console
478
- console = Console()
479
-
480
- # Initialize conversation context
481
- context: list[BaseMessage] = []
482
- if system_instruction:
483
- if isinstance(system_instruction, BaseMessage):
484
- context.append(system_instruction)
485
- else:
486
- context.extend(system_instruction)
487
-
488
- # Display welcome message
489
- console.print("[bold blue]Welcome to the Interactive Chatterer Shell![/bold blue]")
490
- console.print("Type 'quit' or 'exit' to end the conversation.")
491
-
492
- while True:
493
- # Get user input
494
- user_input = Prompt.ask("[bold green]You[/bold green]")
495
- if user_input.lower() in ["quit", "exit"]:
496
- console.print("[bold blue]Goodbye![/bold blue]")
497
- break
498
-
499
- # Add user message to context
500
- context.append(HumanMessage(content=user_input))
501
-
502
- # Determine if code execution is needed
503
- decision = chatterer.generate_pydantic(
504
- response_model=IsCodeExecutionNeeded, # Use response_model instead of pydantic_model
505
- messages=augment_prompt_for_toolcall(
506
- function_signatures=function_signatures,
507
- messages=context,
508
- prompt_for_code_invoke=prompt_for_code_invoke,
509
- function_reference_prefix=function_reference_prefix,
510
- function_reference_seperator=function_reference_seperator,
511
- ),
512
- )
513
-
514
- if decision.is_code_execution_needed:
515
- # Execute code if needed
516
- code_result = chatterer.invoke_code_execution(
517
- messages=context,
518
- repl_tool=repl_tool,
519
- prompt_for_code_invoke=prompt_for_code_invoke,
520
- function_signatures=function_signatures,
521
- function_reference_prefix=function_reference_prefix,
522
- function_reference_seperator=function_reference_seperator,
523
- config=config,
524
- stop=stop,
525
- **kwargs,
526
- )
527
-
528
- if code_result.code.strip() == "pass":
529
- tool_use_message = None
530
- else:
531
- code_session_messages: list[BaseMessage] = []
532
- while True:
533
- code_execution_message = SystemMessage(
534
- content=f"Executed code:\n```python\n{code_result.code}\n```\nOutput:\n{code_result.output}"
535
- )
536
- code_session_messages.append(code_execution_message)
537
- console.print("[bold yellow]Executed code:[/bold yellow]")
538
- console.print(f"[code]{code_result.code}[/code]")
539
- console.print("[bold yellow]Output:[/bold yellow]")
540
- console.print(code_result.output)
541
-
542
- decision = chatterer.generate_pydantic(
543
- response_model=IsFurtherCodeExecutionNeeded, # Use response_model instead of pydantic_model
544
- messages=augment_prompt_for_toolcall(
545
- function_signatures=function_signatures,
546
- messages=context + code_session_messages,
547
- prompt_for_code_invoke=prompt_for_code_invoke,
548
- function_reference_prefix=function_reference_prefix,
549
- function_reference_seperator=function_reference_seperator,
550
- ),
551
- )
552
- review_on_code_execution = decision.review_on_code_execution
553
- next_action = decision.next_action
554
- console.print("[bold blue]AI:[/bold blue]")
555
- console.print(f"-[bold yellow]Review on code execution:[/bold yellow] {review_on_code_execution}")
556
- console.print(f"-[bold yellow]Next Action:[/bold yellow] {next_action}")
557
- code_session_messages.append(
558
- AIMessage(
559
- content=f"- Review upon code execution: {decision.review_on_code_execution}\n- Next Action: {decision.next_action}"
560
- )
561
- )
562
- if not decision.is_further_code_execution_needed:
563
- tool_use_message = code_execution_message
564
- break
565
- else:
566
- # No code execution required
567
- tool_use_message = None
568
-
569
- # Add system message to context
570
- if tool_use_message:
571
- context.append(tool_use_message)
572
-
573
- # Generate and display chatbot response
574
- response = chatterer.generate(messages=context) # Use generate instead of generate_response
575
- context.append(AIMessage(content=response))
576
- console.print(f"[bold blue]AI:[/bold blue] {response}")
577
-
578
-
579
- if __name__ == "__main__":
580
- interactive_shell()
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, get_default_repl_tool
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
+ "\n"
39
+ "To optimize tool efficiency, follow these guidelines:\n"
40
+ "- Write concise, efficient code that directly serves the intended purpose.\n"
41
+ "- Avoid unnecessary operations (e.g., excessive loops, recursion, or heavy computations).\n"
42
+ "- Handle potential errors gracefully (e.g., using try-except blocks).\n"
43
+ "\n"
44
+ "Return your response strictly in the following JSON format:\n"
45
+ '{\n "code": "<your_python_code_here>"\n}\n\n'
46
+ )
47
+
48
+
49
+ DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT = (
50
+ "Below functions are included in global scope and can be used in your code.\n"
51
+ "Do not try to redefine the function(s).\n"
52
+ "You don't have to force yourself to use these tools - use them only when you need to.\n"
53
+ )
54
+ DEFAULT_FUNCTION_REFERENCE_SEPARATOR = "\n---\n" # Separator to distinguish different function references
55
+
56
+
57
+ class Chatterer(BaseModel):
58
+ """Language model for generating text from a given input."""
59
+
60
+ client: BaseChatModel
61
+ structured_output_kwargs: dict[str, Any] = Field(default_factory=dict)
62
+
63
+ @overload
64
+ def __call__(
65
+ self,
66
+ messages: LanguageModelInput,
67
+ response_model: Type[PydanticModelT],
68
+ config: Optional[RunnableConfig] = None,
69
+ stop: Optional[list[str]] = None,
70
+ **kwargs: Any,
71
+ ) -> PydanticModelT: ...
72
+
73
+ @overload
74
+ def __call__(
75
+ self,
76
+ messages: LanguageModelInput,
77
+ response_model: None = None,
78
+ config: Optional[RunnableConfig] = None,
79
+ stop: Optional[list[str]] = None,
80
+ **kwargs: Any,
81
+ ) -> str: ...
82
+
83
+ def __call__(
84
+ self,
85
+ messages: LanguageModelInput,
86
+ response_model: Optional[Type[PydanticModelT]] = None,
87
+ config: Optional[RunnableConfig] = None,
88
+ stop: Optional[list[str]] = None,
89
+ **kwargs: Any,
90
+ ) -> str | PydanticModelT:
91
+ if response_model:
92
+ return self.generate_pydantic(response_model, messages, config, stop, **kwargs)
93
+ return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
94
+
95
+ @classmethod
96
+ def openai(
97
+ cls,
98
+ model: str = "gpt-4o-mini",
99
+ structured_output_kwargs: Optional[dict[str, Any]] = {"strict": True},
100
+ ) -> Self:
101
+ from langchain_openai import ChatOpenAI
102
+
103
+ return cls(client=ChatOpenAI(model=model), structured_output_kwargs=structured_output_kwargs or {})
104
+
105
+ @classmethod
106
+ def anthropic(
107
+ cls,
108
+ model_name: str = "claude-3-7-sonnet-20250219",
109
+ structured_output_kwargs: Optional[dict[str, Any]] = None,
110
+ ) -> Self:
111
+ from langchain_anthropic import ChatAnthropic
112
+
113
+ return cls(
114
+ client=ChatAnthropic(model_name=model_name, timeout=None, stop=None),
115
+ structured_output_kwargs=structured_output_kwargs or {},
116
+ )
117
+
118
+ @classmethod
119
+ def google(
120
+ cls,
121
+ model: str = "gemini-2.0-flash",
122
+ structured_output_kwargs: Optional[dict[str, Any]] = None,
123
+ ) -> Self:
124
+ from langchain_google_genai import ChatGoogleGenerativeAI
125
+
126
+ return cls(
127
+ client=ChatGoogleGenerativeAI(model=model),
128
+ structured_output_kwargs=structured_output_kwargs or {},
129
+ )
130
+
131
+ @classmethod
132
+ def ollama(
133
+ cls,
134
+ model: str = "deepseek-r1:1.5b",
135
+ structured_output_kwargs: Optional[dict[str, Any]] = None,
136
+ ) -> Self:
137
+ from langchain_ollama import ChatOllama
138
+
139
+ return cls(
140
+ client=ChatOllama(model=model),
141
+ structured_output_kwargs=structured_output_kwargs or {},
142
+ )
143
+
144
+ def generate(
145
+ self,
146
+ messages: LanguageModelInput,
147
+ config: Optional[RunnableConfig] = None,
148
+ stop: Optional[list[str]] = None,
149
+ **kwargs: Any,
150
+ ) -> str:
151
+ return self.client.invoke(input=messages, config=config, stop=stop, **kwargs).text()
152
+
153
+ async def agenerate(
154
+ self,
155
+ messages: LanguageModelInput,
156
+ config: Optional[RunnableConfig] = None,
157
+ stop: Optional[list[str]] = None,
158
+ **kwargs: Any,
159
+ ) -> str:
160
+ return (await self.client.ainvoke(input=messages, config=config, stop=stop, **kwargs)).text()
161
+
162
+ def generate_stream(
163
+ self,
164
+ messages: LanguageModelInput,
165
+ config: Optional[RunnableConfig] = None,
166
+ stop: Optional[list[str]] = None,
167
+ **kwargs: Any,
168
+ ) -> Iterator[str]:
169
+ for chunk in self.client.stream(input=messages, config=config, stop=stop, **kwargs):
170
+ yield chunk.text()
171
+
172
+ async def agenerate_stream(
173
+ self,
174
+ messages: LanguageModelInput,
175
+ config: Optional[RunnableConfig] = None,
176
+ stop: Optional[list[str]] = None,
177
+ **kwargs: Any,
178
+ ) -> AsyncIterator[str]:
179
+ async for chunk in self.client.astream(input=messages, config=config, stop=stop, **kwargs):
180
+ yield chunk.text()
181
+
182
+ def generate_pydantic(
183
+ self,
184
+ response_model: Type[PydanticModelT],
185
+ messages: LanguageModelInput,
186
+ config: Optional[RunnableConfig] = None,
187
+ stop: Optional[list[str]] = None,
188
+ **kwargs: Any,
189
+ ) -> PydanticModelT:
190
+ result: StructuredOutputType = _with_structured_output(
191
+ client=self.client,
192
+ response_model=response_model,
193
+ structured_output_kwargs=self.structured_output_kwargs,
194
+ ).invoke(input=messages, config=config, stop=stop, **kwargs)
195
+ if isinstance(result, response_model):
196
+ return result
197
+ else:
198
+ return response_model.model_validate(result)
199
+
200
+ async def agenerate_pydantic(
201
+ self,
202
+ response_model: Type[PydanticModelT],
203
+ messages: LanguageModelInput,
204
+ config: Optional[RunnableConfig] = None,
205
+ stop: Optional[list[str]] = None,
206
+ **kwargs: Any,
207
+ ) -> PydanticModelT:
208
+ result: StructuredOutputType = await _with_structured_output(
209
+ client=self.client,
210
+ response_model=response_model,
211
+ structured_output_kwargs=self.structured_output_kwargs,
212
+ ).ainvoke(input=messages, config=config, stop=stop, **kwargs)
213
+ if isinstance(result, response_model):
214
+ return result
215
+ else:
216
+ return response_model.model_validate(result)
217
+
218
+ def generate_pydantic_stream(
219
+ self,
220
+ response_model: Type[PydanticModelT],
221
+ messages: LanguageModelInput,
222
+ config: Optional[RunnableConfig] = None,
223
+ stop: Optional[list[str]] = None,
224
+ **kwargs: Any,
225
+ ) -> Iterator[PydanticModelT]:
226
+ try:
227
+ import instructor
228
+ except ImportError:
229
+ raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
230
+
231
+ partial_response_model = instructor.Partial[response_model]
232
+ for chunk in _with_structured_output(
233
+ client=self.client,
234
+ response_model=partial_response_model,
235
+ structured_output_kwargs=self.structured_output_kwargs,
236
+ ).stream(input=messages, config=config, stop=stop, **kwargs):
237
+ yield response_model.model_validate(chunk)
238
+
239
+ async def agenerate_pydantic_stream(
240
+ self,
241
+ response_model: Type[PydanticModelT],
242
+ messages: LanguageModelInput,
243
+ config: Optional[RunnableConfig] = None,
244
+ stop: Optional[list[str]] = None,
245
+ **kwargs: Any,
246
+ ) -> AsyncIterator[PydanticModelT]:
247
+ try:
248
+ import instructor
249
+ except ImportError:
250
+ raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
251
+
252
+ partial_response_model = instructor.Partial[response_model]
253
+ async for chunk in _with_structured_output(
254
+ client=self.client,
255
+ response_model=partial_response_model,
256
+ structured_output_kwargs=self.structured_output_kwargs,
257
+ ).astream(input=messages, config=config, stop=stop, **kwargs):
258
+ yield response_model.model_validate(chunk)
259
+
260
+ def describe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
261
+ """
262
+ Create a detailed description of an image using the Vision Language Model.
263
+ - image_url: Image URL to describe
264
+ """
265
+ return self.generate([
266
+ HumanMessage(
267
+ content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
268
+ )
269
+ ])
270
+
271
+ async def adescribe_image(self, image_url: str, instruction: str = DEFAULT_IMAGE_DESCRIPTION_INSTRUCTION) -> str:
272
+ """
273
+ Create a detailed description of an image using the Vision Language Model asynchronously.
274
+ - image_url: Image URL to describe
275
+ """
276
+ return await self.agenerate([
277
+ HumanMessage(
278
+ content=[{"type": "text", "text": instruction}, {"type": "image_url", "image_url": {"url": image_url}}],
279
+ )
280
+ ])
281
+
282
+ @staticmethod
283
+ def get_num_tokens_from_message(message: BaseMessage) -> Optional[tuple[int, int]]:
284
+ try:
285
+ if isinstance(message, AIMessage) and (usage_metadata := message.usage_metadata):
286
+ input_tokens = int(usage_metadata["input_tokens"])
287
+ output_tokens = int(usage_metadata["output_tokens"])
288
+ else:
289
+ # Dynamic extraction for unknown structures
290
+ input_tokens: Optional[int] = None
291
+ output_tokens: Optional[int] = None
292
+
293
+ def _find_tokens(obj: object) -> None:
294
+ nonlocal input_tokens, output_tokens
295
+ if isinstance(obj, dict):
296
+ for key, value in cast(dict[object, object], obj).items():
297
+ if isinstance(value, int):
298
+ if "input" in str(key) or "prompt" in str(key):
299
+ input_tokens = value
300
+ elif "output" in str(key) or "completion" in str(key):
301
+ output_tokens = value
302
+ else:
303
+ _find_tokens(value)
304
+ elif isinstance(obj, list):
305
+ for item in cast(list[object], obj):
306
+ _find_tokens(item)
307
+
308
+ _find_tokens(message.model_dump())
309
+
310
+ if input_tokens is None or output_tokens is None:
311
+ return None
312
+ return input_tokens, output_tokens
313
+ except Exception:
314
+ return None
315
+
316
+ def invoke_code_execution(
317
+ self,
318
+ messages: LanguageModelInput,
319
+ repl_tool: Optional["PythonAstREPLTool"] = None,
320
+ prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
321
+ function_signatures: Optional[FunctionSignature | Iterable[FunctionSignature]] = None,
322
+ function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
323
+ function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
324
+ config: Optional[RunnableConfig] = None,
325
+ stop: Optional[list[str]] = None,
326
+ **kwargs: Any,
327
+ ) -> CodeExecutionResult:
328
+ if not function_signatures:
329
+ function_signatures = []
330
+ elif isinstance(function_signatures, FunctionSignature):
331
+ function_signatures = [function_signatures]
332
+ messages = augment_prompt_for_toolcall(
333
+ function_signatures=function_signatures,
334
+ messages=messages,
335
+ prompt_for_code_invoke=prompt_for_code_invoke,
336
+ function_reference_prefix=function_reference_prefix,
337
+ function_reference_seperator=function_reference_seperator,
338
+ )
339
+ code_obj: PythonCodeToExecute = self.generate_pydantic(
340
+ response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
341
+ )
342
+ return CodeExecutionResult.from_code(
343
+ code=code_obj.code,
344
+ config=config,
345
+ repl_tool=repl_tool,
346
+ function_signatures=function_signatures,
347
+ **kwargs,
348
+ )
349
+
350
+ async def ainvoke_code_execution(
351
+ self,
352
+ messages: LanguageModelInput,
353
+ repl_tool: Optional["PythonAstREPLTool"] = None,
354
+ prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
355
+ additional_callables: Optional[Callable[..., object] | Sequence[Callable[..., object]]] = None,
356
+ function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
357
+ function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
358
+ config: Optional[RunnableConfig] = None,
359
+ stop: Optional[list[str]] = None,
360
+ **kwargs: Any,
361
+ ) -> CodeExecutionResult:
362
+ function_signatures: list[FunctionSignature] = FunctionSignature.from_callable(additional_callables)
363
+ messages = augment_prompt_for_toolcall(
364
+ function_signatures=function_signatures,
365
+ messages=messages,
366
+ prompt_for_code_invoke=prompt_for_code_invoke,
367
+ function_reference_prefix=function_reference_prefix,
368
+ function_reference_seperator=function_reference_seperator,
369
+ )
370
+ code_obj: PythonCodeToExecute = await self.agenerate_pydantic(
371
+ response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
372
+ )
373
+ return await CodeExecutionResult.afrom_code(
374
+ code=code_obj.code,
375
+ config=config,
376
+ repl_tool=repl_tool,
377
+ function_signatures=function_signatures,
378
+ **kwargs,
379
+ )
380
+
381
+
382
+ class PythonCodeToExecute(BaseModel):
383
+ code: str = Field(description="Python code to execute")
384
+
385
+
386
+ def _with_structured_output(
387
+ client: BaseChatModel,
388
+ response_model: Type["PydanticModelT | Partial[PydanticModelT]"],
389
+ structured_output_kwargs: dict[str, Any],
390
+ ) -> Runnable[LanguageModelInput, dict[object, object] | BaseModel]:
391
+ return client.with_structured_output(schema=response_model, **structured_output_kwargs) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
392
+
393
+
394
+ # def _add_message_last(messages: LanguageModelInput, prompt_to_add: str) -> LanguageModelInput:
395
+ # if isinstance(messages, str):
396
+ # messages += f"\n{prompt_to_add}"
397
+ # elif isinstance(messages, Sequence):
398
+ # messages = list(messages)
399
+ # messages.append(SystemMessage(content=prompt_to_add))
400
+ # else:
401
+ # messages = messages.to_messages()
402
+ # messages.append(SystemMessage(content=prompt_to_add))
403
+ # return messages
404
+
405
+
406
+ def _add_message_first(messages: LanguageModelInput, prompt_to_add: str) -> LanguageModelInput:
407
+ if isinstance(messages, str):
408
+ messages = f"{prompt_to_add}\n{messages}"
409
+ elif isinstance(messages, Sequence):
410
+ messages = list(messages)
411
+ messages.insert(0, SystemMessage(content=prompt_to_add))
412
+ else:
413
+ messages = messages.to_messages()
414
+ messages.insert(0, SystemMessage(content=prompt_to_add))
415
+ return messages
416
+
417
+
418
+ def augment_prompt_for_toolcall(
419
+ function_signatures: Iterable[FunctionSignature],
420
+ messages: LanguageModelInput,
421
+ prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
422
+ function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
423
+ function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
424
+ ) -> LanguageModelInput:
425
+ if function_signatures:
426
+ messages = _add_message_first(
427
+ messages=messages,
428
+ prompt_to_add=FunctionSignature.as_prompt(
429
+ function_signatures, function_reference_prefix, function_reference_seperator
430
+ ),
431
+ )
432
+ if prompt_for_code_invoke:
433
+ messages = _add_message_first(messages=messages, prompt_to_add=prompt_for_code_invoke)
434
+ return messages
435
+
436
+
437
+ def interactive_shell(
438
+ chatterer: Chatterer = Chatterer.openai(),
439
+ system_instruction: BaseMessage | Iterable[BaseMessage] = ([
440
+ SystemMessage("You are an AI that can answer questions and execute Python code."),
441
+ ]),
442
+ repl_tool: Optional["PythonAstREPLTool"] = None,
443
+ prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
444
+ additional_callables: Optional[Callable[..., object] | Sequence[Callable[..., object]]] = None,
445
+ function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
446
+ function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
447
+ config: Optional[RunnableConfig] = None,
448
+ stop: Optional[list[str]] = None,
449
+ **kwargs: Any,
450
+ ) -> None:
451
+ from rich.console import Console
452
+ from rich.prompt import Prompt
453
+
454
+ # 코드 실행 필요 여부를 판단하는 모델
455
+ class IsCodeExecutionNeeded(BaseModel):
456
+ is_code_execution_needed: bool = Field(
457
+ description="Whether Python tool calling is needed to answer user query."
458
+ )
459
+
460
+ # 추가 코드 실행 필요 여부를 판단하는 모델
461
+ class IsFurtherCodeExecutionNeeded(BaseModel):
462
+ review_on_code_execution: str = Field(description="Review on the code execution.")
463
+ next_action: str = Field(description="Next action to take.")
464
+ is_further_code_execution_needed: bool = Field(
465
+ description="Whether further Python tool calling is needed to answer user query."
466
+ )
467
+
468
+ def respond(messages: list[BaseMessage]) -> str:
469
+ # AI 응답 스트리밍 출력
470
+ console.print("[bold blue]AI:[/bold blue] ", end="")
471
+ response = ""
472
+ for chunk in chatterer.generate_stream(messages=messages):
473
+ response += chunk
474
+ console.print(chunk, end="")
475
+ console.print() # 응답 줄바꿈 추가
476
+ return response.strip()
477
+
478
+ def code_session_returning_end_of_turn() -> bool:
479
+ code_session_messages: list[BaseMessage] = []
480
+ while True:
481
+ code_execution: CodeExecutionResult = chatterer.invoke_code_execution(
482
+ messages=context,
483
+ repl_tool=repl_tool,
484
+ prompt_for_code_invoke=prompt_for_code_invoke,
485
+ function_signatures=function_signatures,
486
+ function_reference_prefix=function_reference_prefix,
487
+ function_reference_seperator=function_reference_seperator,
488
+ config=config,
489
+ stop=stop,
490
+ **kwargs,
491
+ )
492
+ if code_execution.code.strip() in ("", "quit", "exit", "pass"):
493
+ return False
494
+
495
+ last_tool_use_message = AIMessage(
496
+ content=f"Executed code:\n```python\n{code_execution.code}\n```\nOutput:\n{code_execution.output}".strip()
497
+ )
498
+ code_session_messages.append(last_tool_use_message)
499
+ console.print("[bold yellow]Executed code:[/bold yellow]")
500
+ console.print(f"[code]{code_execution.code}[/code]")
501
+ console.print("[bold yellow]Output:[/bold yellow]")
502
+ console.print(code_execution.output)
503
+
504
+ decision = chatterer.generate_pydantic(
505
+ response_model=IsFurtherCodeExecutionNeeded,
506
+ messages=augment_prompt_for_toolcall(
507
+ function_signatures=function_signatures,
508
+ messages=context + code_session_messages,
509
+ prompt_for_code_invoke=prompt_for_code_invoke,
510
+ function_reference_prefix=function_reference_prefix,
511
+ function_reference_seperator=function_reference_seperator,
512
+ ),
513
+ )
514
+ review_on_code_execution = decision.review_on_code_execution.strip()
515
+ next_action = decision.next_action.strip()
516
+ console.print("[bold blue]AI:[/bold blue]")
517
+ console.print(f"-[bold yellow]Review on code execution:[/bold yellow] {review_on_code_execution}")
518
+ console.print(f"-[bold yellow]Next Action:[/bold yellow] {next_action}")
519
+ code_session_messages.append(
520
+ AIMessage(
521
+ content=f"- Review upon code execution: {review_on_code_execution}\n- Next Action: {next_action}".strip()
522
+ )
523
+ )
524
+ if not decision.is_further_code_execution_needed:
525
+ response: str = respond(context + code_session_messages)
526
+ context.append(last_tool_use_message)
527
+ context.append(AIMessage(content=response))
528
+ return True
529
+
530
+ # REPL 도구 초기화
531
+ if repl_tool is None:
532
+ repl_tool = get_default_repl_tool()
533
+
534
+ function_signatures: list[FunctionSignature] = FunctionSignature.from_callable(additional_callables)
535
+ console = Console()
536
+ context: list[BaseMessage] = []
537
+ if system_instruction:
538
+ if isinstance(system_instruction, BaseMessage):
539
+ context.append(system_instruction)
540
+ else:
541
+ context.extend(system_instruction)
542
+
543
+ # 환영 메시지
544
+ console.print("[bold blue]Welcome to the Interactive Chatterer Shell![/bold blue]")
545
+ console.print("Type 'quit' or 'exit' to end the conversation.")
546
+
547
+ while True:
548
+ # 사용자 입력 받기
549
+ user_input = Prompt.ask("[bold green]You[/bold green]")
550
+ if user_input.lower() in ["quit", "exit"]:
551
+ console.print("[bold blue]Goodbye![/bold blue]")
552
+ break
553
+
554
+ context.append(HumanMessage(content=user_input))
555
+
556
+ # 코드 실행 필요 여부 판단
557
+ decision = chatterer.generate_pydantic(
558
+ response_model=IsCodeExecutionNeeded,
559
+ messages=augment_prompt_for_toolcall(
560
+ function_signatures=function_signatures,
561
+ messages=context,
562
+ prompt_for_code_invoke=prompt_for_code_invoke,
563
+ function_reference_prefix=function_reference_prefix,
564
+ function_reference_seperator=function_reference_seperator,
565
+ ),
566
+ )
567
+
568
+ # 코드 실행 처리
569
+ if decision.is_code_execution_needed and code_session_returning_end_of_turn():
570
+ continue
571
+
572
+ # AI 응답 스트리밍 출력
573
+ context.append(AIMessage(content=respond(context)))
574
+
575
+
576
+ if __name__ == "__main__":
577
+ interactive_shell()