chatterer 0.1.11__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,581 +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
- "\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
- # REPL 도구 초기화
469
- if repl_tool is None:
470
- repl_tool = get_default_repl_tool()
471
-
472
- function_signatures: list[FunctionSignature] = FunctionSignature.from_callable(additional_callables)
473
- console = Console()
474
- context: list[BaseMessage] = []
475
- if system_instruction:
476
- if isinstance(system_instruction, BaseMessage):
477
- context.append(system_instruction)
478
- else:
479
- context.extend(system_instruction)
480
-
481
- # 환영 메시지
482
- console.print("[bold blue]Welcome to the Interactive Chatterer Shell![/bold blue]")
483
- console.print("Type 'quit' or 'exit' to end the conversation.")
484
-
485
- while True:
486
- # 사용자 입력 받기
487
- user_input = Prompt.ask("[bold green]You[/bold green]")
488
- if user_input.lower() in ["quit", "exit"]:
489
- console.print("[bold blue]Goodbye![/bold blue]")
490
- break
491
-
492
- context.append(HumanMessage(content=user_input))
493
-
494
- # 코드 실행 필요 여부 판단
495
- decision = chatterer.generate_pydantic(
496
- response_model=IsCodeExecutionNeeded,
497
- messages=augment_prompt_for_toolcall(
498
- function_signatures=function_signatures,
499
- messages=context,
500
- prompt_for_code_invoke=prompt_for_code_invoke,
501
- function_reference_prefix=function_reference_prefix,
502
- function_reference_seperator=function_reference_seperator,
503
- ),
504
- )
505
-
506
- # 코드 실행 처리
507
- if decision.is_code_execution_needed:
508
- code_result = chatterer.invoke_code_execution(
509
- messages=context,
510
- repl_tool=repl_tool,
511
- prompt_for_code_invoke=prompt_for_code_invoke,
512
- function_signatures=function_signatures,
513
- function_reference_prefix=function_reference_prefix,
514
- function_reference_seperator=function_reference_seperator,
515
- config=config,
516
- stop=stop,
517
- **kwargs,
518
- )
519
-
520
- if code_result.code.strip() == "pass":
521
- tool_use_message = None
522
- else:
523
- code_session_messages: list[BaseMessage] = []
524
- while True:
525
- code_execution_message = AIMessage(
526
- content=f"Executed code:\n```python\n{code_result.code}\n```\nOutput:\n{code_result.output}".strip()
527
- )
528
- code_session_messages.append(code_execution_message)
529
- console.print("[bold yellow]Executed code:[/bold yellow]")
530
- console.print(f"[code]{code_result.code}[/code]")
531
- console.print("[bold yellow]Output:[/bold yellow]")
532
- console.print(code_result.output)
533
-
534
- decision = chatterer.generate_pydantic(
535
- response_model=IsFurtherCodeExecutionNeeded,
536
- messages=augment_prompt_for_toolcall(
537
- function_signatures=function_signatures,
538
- messages=context + code_session_messages,
539
- prompt_for_code_invoke=prompt_for_code_invoke,
540
- function_reference_prefix=function_reference_prefix,
541
- function_reference_seperator=function_reference_seperator,
542
- ),
543
- )
544
- review_on_code_execution = decision.review_on_code_execution.strip()
545
- next_action = decision.next_action.strip()
546
- console.print("[bold blue]AI:[/bold blue]")
547
- console.print(f"-[bold yellow]Review on code execution:[/bold yellow] {review_on_code_execution}")
548
- console.print(f"-[bold yellow]Next Action:[/bold yellow] {next_action}")
549
- code_session_messages.append(
550
- AIMessage(
551
- content=f"- Review upon code execution: {review_on_code_execution}\n- Next Action: {next_action}".strip()
552
- )
553
- )
554
- if not decision.is_further_code_execution_needed:
555
- tool_use_message = code_execution_message
556
- break
557
- else:
558
- tool_use_message = None
559
-
560
- # 코드 실행 결과 컨텍스트에 추가
561
- if tool_use_message:
562
- context.append(tool_use_message)
563
-
564
- # AI 응답 스트리밍 출력
565
- console.print("[bold blue]AI:[/bold blue] ", end="")
566
- response = ""
567
- for chunk in chatterer.generate_stream(messages=context):
568
- response += chunk
569
- console.print(chunk, end="")
570
-
571
- # 전체 응답 처리 후 컨텍스트에 추가
572
- lines = response.split("\n")
573
- if lines:
574
- lines[-1] = lines[-1].rstrip() # 마지막 줄의 오른쪽 공백 제거
575
- response = "\n".join(lines).strip()
576
- context.append(AIMessage(content=response))
577
- console.print() # 응답 후 줄바꿈 추가
578
-
579
-
580
- if __name__ == "__main__":
581
- 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()