chatterer 0.1.9__py3-none-any.whl → 0.1.10__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 CHANGED
@@ -1,4 +1,4 @@
1
- from .language_model import Chatterer
1
+ from .language_model import Chatterer, interactive_shell
2
2
  from .messages import (
3
3
  AIMessage,
4
4
  BaseMessage,
@@ -16,10 +16,12 @@ from .tools import (
16
16
  anything_to_markdown,
17
17
  citation_chunker,
18
18
  get_default_html_to_markdown_options,
19
+ get_youtube_video_subtitle,
19
20
  html_to_markdown,
20
21
  init_webpage_to_markdown,
21
22
  pdf_to_text,
22
23
  pyscripts_to_snippets,
24
+ get_youtube_video_details,
23
25
  )
24
26
  from .utils import (
25
27
  Base64Image,
@@ -52,4 +54,7 @@ __all__ = [
52
54
  "CodeExecutionResult",
53
55
  "get_default_repl_tool",
54
56
  "insert_callables_into_global",
57
+ "get_youtube_video_subtitle",
58
+ "get_youtube_video_details",
59
+ "interactive_shell",
55
60
  ]
@@ -22,7 +22,7 @@ from langchain_core.runnables.config import RunnableConfig
22
22
  from pydantic import BaseModel, Field
23
23
 
24
24
  from .messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
25
- from .utils.code_agent import CodeExecutionResult, FunctionSignature
25
+ from .utils.code_agent import CodeExecutionResult, FunctionSignature, get_default_repl_tool
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  from instructor import Partial
@@ -189,7 +189,7 @@ class Chatterer(BaseModel):
189
189
  stop: Optional[list[str]] = None,
190
190
  **kwargs: Any,
191
191
  ) -> PydanticModelT:
192
- result: StructuredOutputType = with_structured_output(
192
+ result: StructuredOutputType = _with_structured_output(
193
193
  client=self.client,
194
194
  response_model=response_model,
195
195
  structured_output_kwargs=self.structured_output_kwargs,
@@ -207,7 +207,7 @@ class Chatterer(BaseModel):
207
207
  stop: Optional[list[str]] = None,
208
208
  **kwargs: Any,
209
209
  ) -> PydanticModelT:
210
- result: StructuredOutputType = await with_structured_output(
210
+ result: StructuredOutputType = await _with_structured_output(
211
211
  client=self.client,
212
212
  response_model=response_model,
213
213
  structured_output_kwargs=self.structured_output_kwargs,
@@ -231,7 +231,7 @@ class Chatterer(BaseModel):
231
231
  raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
232
232
 
233
233
  partial_response_model = instructor.Partial[response_model]
234
- for chunk in with_structured_output(
234
+ for chunk in _with_structured_output(
235
235
  client=self.client,
236
236
  response_model=partial_response_model,
237
237
  structured_output_kwargs=self.structured_output_kwargs,
@@ -252,7 +252,7 @@ class Chatterer(BaseModel):
252
252
  raise ImportError("Please install `instructor` with `pip install instructor` to use this feature.")
253
253
 
254
254
  partial_response_model = instructor.Partial[response_model]
255
- async for chunk in with_structured_output(
255
+ async for chunk in _with_structured_output(
256
256
  client=self.client,
257
257
  response_model=partial_response_model,
258
258
  structured_output_kwargs=self.structured_output_kwargs,
@@ -320,26 +320,24 @@ class Chatterer(BaseModel):
320
320
  messages: LanguageModelInput,
321
321
  repl_tool: Optional["PythonAstREPLTool"] = None,
322
322
  prompt_for_code_invoke: Optional[str] = DEFAULT_CODE_GENERATION_PROMPT,
323
- additional_callables: Optional[Callable[..., object] | Sequence[Callable[..., object]]] = None,
323
+ function_signatures: Optional[FunctionSignature | Iterable[FunctionSignature]] = None,
324
324
  function_reference_prefix: Optional[str] = DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
325
325
  function_reference_seperator: str = DEFAULT_FUNCTION_REFERENCE_SEPARATOR,
326
326
  config: Optional[RunnableConfig] = None,
327
327
  stop: Optional[list[str]] = None,
328
328
  **kwargs: Any,
329
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)
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
+ )
343
341
  code_obj: PythonCodeToExecute = self.generate_pydantic(
344
342
  response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
345
343
  )
@@ -363,19 +361,14 @@ class Chatterer(BaseModel):
363
361
  stop: Optional[list[str]] = None,
364
362
  **kwargs: Any,
365
363
  ) -> 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)
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
+ )
379
372
  code_obj: PythonCodeToExecute = await self.agenerate_pydantic(
380
373
  response_model=PythonCodeToExecute, messages=messages, config=config, stop=stop, **kwargs
381
374
  )
@@ -392,7 +385,7 @@ class PythonCodeToExecute(BaseModel):
392
385
  code: str = Field(description="Python code to execute")
393
386
 
394
387
 
395
- def with_structured_output(
388
+ def _with_structured_output(
396
389
  client: BaseChatModel,
397
390
  response_model: Type["PydanticModelT | Partial[PydanticModelT]"],
398
391
  structured_output_kwargs: dict[str, Any],
@@ -424,25 +417,76 @@ def _add_message_last(messages: LanguageModelInput, prompt_to_add: str) -> Langu
424
417
  # return messages
425
418
 
426
419
 
427
- def chatbot_example(chatterer: Chatterer = Chatterer.openai()) -> None:
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:
428
453
  # Define the CodeExecutionDecision class using Pydantic
429
454
 
430
455
  from rich.console import Console
431
456
  from rich.prompt import Prompt
432
457
 
433
- class CodeExecutionDecision(BaseModel):
458
+ class IsCodeExecutionNeeded(BaseModel):
434
459
  is_code_execution_needed: bool = Field(
435
460
  description="Whether Python tool calling is needed to answer user query."
436
461
  )
437
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
+
438
477
  # Initialize Rich console
439
478
  console = Console()
440
479
 
441
480
  # Initialize conversation context
442
- context: list[BaseMessage] = [SystemMessage("You are an AI that can answer questions and execute Python code.")]
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)
443
487
 
444
488
  # Display welcome message
445
- console.print("[bold blue]Welcome to the Rich-based chatbot![/bold blue]")
489
+ console.print("[bold blue]Welcome to the Interactive Chatterer Shell![/bold blue]")
446
490
  console.print("Type 'quit' or 'exit' to end the conversation.")
447
491
 
448
492
  while True:
@@ -457,36 +501,80 @@ def chatbot_example(chatterer: Chatterer = Chatterer.openai()) -> None:
457
501
 
458
502
  # Determine if code execution is needed
459
503
  decision = chatterer.generate_pydantic(
460
- response_model=CodeExecutionDecision, # Use response_model instead of pydantic_model
461
- messages=context,
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
+ ),
462
512
  )
463
513
 
464
514
  if decision.is_code_execution_needed:
465
515
  # Execute code if needed
466
- code_result = chatterer.invoke_code_execution(messages=context)
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
+
467
528
  if code_result.code.strip() == "pass":
468
- new_message = None
529
+ tool_use_message = None
469
530
  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)
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
477
565
  else:
478
566
  # No code execution required
479
- new_message = None
567
+ tool_use_message = None
480
568
 
481
569
  # Add system message to context
482
- if new_message:
483
- context.append(new_message)
570
+ if tool_use_message:
571
+ context.append(tool_use_message)
484
572
 
485
573
  # Generate and display chatbot response
486
574
  response = chatterer.generate(messages=context) # Use generate instead of generate_response
487
575
  context.append(AIMessage(content=response))
488
- console.print(f"[bold blue]Chatbot:[/bold blue] {response}")
576
+ console.print(f"[bold blue]AI:[/bold blue] {response}")
489
577
 
490
578
 
491
579
  if __name__ == "__main__":
492
- chatbot_example()
580
+ interactive_shell()
@@ -6,6 +6,7 @@ from .convert_to_text import (
6
6
  pdf_to_text,
7
7
  pyscripts_to_snippets,
8
8
  )
9
+ from .youtube import get_youtube_video_subtitle, get_youtube_video_details
9
10
 
10
11
 
11
12
  def init_webpage_to_markdown():
@@ -22,4 +23,6 @@ __all__ = [
22
23
  "pyscripts_to_snippets",
23
24
  "citation_chunker",
24
25
  "init_webpage_to_markdown",
26
+ "get_youtube_video_subtitle",
27
+ "get_youtube_video_details",
25
28
  ]
@@ -0,0 +1,132 @@
1
+ import json
2
+ import unicodedata
3
+ import urllib.parse
4
+ from dataclasses import dataclass
5
+ from typing import Any, Optional, Self, cast
6
+
7
+ import requests
8
+
9
+
10
+ def get_youtube_video_details(
11
+ query: str,
12
+ ) -> list[dict[str, Optional[str]]]:
13
+ """Search for video metadata on YouTube using the given query. Returns a list of dictionaries containing `video_id`, `title`, `channel`, `duration`, `views`, `publish_time`, and `long_desc`."""
14
+ return [
15
+ {
16
+ "video_id": video_id,
17
+ "title": video.title,
18
+ "channel": video.channel,
19
+ "duration": video.duration,
20
+ "views": video.views,
21
+ "publish_time": video.publish_time,
22
+ "long_desc": video.long_desc,
23
+ }
24
+ for video in YoutubeSearchResult.from_query(base_url="https://youtube.com", query=query, max_results=10)
25
+ if (video_id := _get_video_id(video.url_suffix))
26
+ ]
27
+
28
+
29
+ def get_youtube_video_subtitle(video_id: str) -> str:
30
+ """Get the transcript of a YouTube video using the given video ID."""
31
+
32
+ from youtube_transcript_api._api import YouTubeTranscriptApi
33
+
34
+ get_transcript = YouTubeTranscriptApi.get_transcript # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
35
+ list_transcripts = YouTubeTranscriptApi.list_transcripts # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
36
+
37
+ result: str = ""
38
+ buffer_timestamp: str = "0s"
39
+ buffer_texts: list[str] = []
40
+ for entry in get_transcript(video_id, languages=(next(iter(list_transcripts(video_id))).language_code,)): # pyright: ignore[reportUnknownVariableType]
41
+ entry = cast(dict[object, object], entry)
42
+ text: str = str(entry.get("text", "")).strip().replace("\n", " ")
43
+ if not text:
44
+ continue
45
+ if len(buffer_texts) >= 10 or _is_special_char(text) or (buffer_texts and _is_special_char(buffer_texts[-1])):
46
+ result += f"[{buffer_timestamp}] {'. '.join(buffer_texts)}\n"
47
+ start = entry.get("start", 0)
48
+ if start:
49
+ buffer_timestamp = f"{start:.0f}s"
50
+ buffer_texts = [text]
51
+ else:
52
+ buffer_texts.append(text)
53
+
54
+ if buffer_texts:
55
+ result += f"[{buffer_timestamp}] {' '.join(buffer_texts)}"
56
+ return result
57
+
58
+
59
+ def _get_video_id(suffix: str) -> str:
60
+ return next(iter(urllib.parse.parse_qs(urllib.parse.urlparse(suffix).query)["v"]), "")
61
+
62
+
63
+ def _is_special_char(text: str) -> bool:
64
+ if not text:
65
+ return False
66
+ return not unicodedata.category(text[0]).startswith("L")
67
+
68
+
69
+ @dataclass
70
+ class YoutubeSearchResult:
71
+ url_suffix: str
72
+ id: Optional[str]
73
+ thumbnails: list[str]
74
+ title: Optional[str]
75
+ long_desc: Optional[str]
76
+ channel: Optional[str]
77
+ duration: Optional[str]
78
+ views: Optional[str]
79
+ publish_time: Optional[str]
80
+
81
+ @classmethod
82
+ def from_query(cls, base_url: str, query: str, max_results: int) -> list[Self]:
83
+ url: str = f"{base_url}/results?search_query={urllib.parse.quote_plus(query)}"
84
+ response: str = requests.get(url).text
85
+ while "ytInitialData" not in response:
86
+ response = requests.get(url).text
87
+ results: list[Self] = cls.parse_html(response)
88
+ return results[:max_results]
89
+
90
+ @classmethod
91
+ def parse_html(cls, html: str) -> list[Self]:
92
+ results: list[Self] = []
93
+ start: int = html.index("ytInitialData") + len("ytInitialData") + 3
94
+ end: int = html.index("};", start) + 1
95
+ data: Any = json.loads(html[start:end])
96
+ for contents in data["contents"]["twoColumnSearchResultsRenderer"]["primaryContents"]["sectionListRenderer"][
97
+ "contents"
98
+ ]:
99
+ for video in contents["itemSectionRenderer"]["contents"]:
100
+ if "videoRenderer" in video.keys():
101
+ video_data = video.get("videoRenderer", {})
102
+ suffix = (
103
+ video_data.get("navigationEndpoint", {})
104
+ .get("commandMetadata", {})
105
+ .get("webCommandMetadata", {})
106
+ .get("url", None)
107
+ )
108
+ if not suffix:
109
+ continue
110
+ res = cls(
111
+ id=video_data.get("videoId", None),
112
+ thumbnails=[
113
+ thumb.get("url", None) for thumb in video_data.get("thumbnail", {}).get("thumbnails", [{}])
114
+ ],
115
+ title=video_data.get("title", {}).get("runs", [[{}]])[0].get("text", None),
116
+ long_desc=video_data.get("descriptionSnippet", {}).get("runs", [{}])[0].get("text", None),
117
+ channel=video_data.get("longBylineText", {}).get("runs", [[{}]])[0].get("text", None),
118
+ duration=video_data.get("lengthText", {}).get("simpleText", 0),
119
+ views=video_data.get("viewCountText", {}).get("simpleText", 0),
120
+ publish_time=video_data.get("publishedTimeText", {}).get("simpleText", 0),
121
+ url_suffix=suffix,
122
+ )
123
+ results.append(res)
124
+
125
+ if results:
126
+ break
127
+ return results
128
+
129
+
130
+ if __name__ == "__main__":
131
+ print(get_youtube_video_details("BTS"))
132
+ # print(get_youtube_transcript("y7jrpS8GHxs"))
@@ -21,7 +21,15 @@ class FunctionSignature(NamedTuple):
21
21
  signature: str
22
22
 
23
23
  @classmethod
24
- def from_callable(cls, callable: Callable[..., object]) -> Self:
24
+ def from_callable(cls, callables: Optional[Callable[..., object] | Iterable[Callable[..., object]]]) -> list[Self]:
25
+ if callables is None:
26
+ return []
27
+ if callable(callables):
28
+ return [cls._from_callable(callables)]
29
+ return [cls._from_callable(callable) for callable in callables]
30
+
31
+ @classmethod
32
+ def _from_callable(cls, callable: Callable[..., object]) -> Self:
25
33
  """
26
34
  Get the name and signature of a function as a string.
27
35
  """
@@ -54,21 +62,17 @@ class FunctionSignature(NamedTuple):
54
62
  else:
55
63
  return cls(name=function_name, callable=callable, signature=signature)
56
64
 
57
- @classmethod
58
- def from_callables(cls, callables: Iterable[Callable[..., object]]) -> list[Self]:
59
- return [cls.from_callable(callable) for callable in callables]
60
-
61
65
  @classmethod
62
66
  def as_prompt(
63
67
  cls,
64
- callables: Iterable[Self],
68
+ function_signatures: Iterable[Self],
65
69
  prefix: Optional[str] = "You can use the pre-made functions below without defining them:\n",
66
70
  sep: str = "\n---\n",
67
71
  ) -> str:
68
72
  """
69
73
  Generate a prompt string from a list of callables.
70
74
  """
71
- body: str = sep.join(fsig.signature for fsig in callables)
75
+ body: str = sep.join(fsig.signature for fsig in function_signatures)
72
76
  if prefix:
73
77
  return f"{prefix}{body}"
74
78
  return body
@@ -92,7 +96,7 @@ class CodeExecutionResult(NamedTuple):
92
96
  """
93
97
  if repl_tool is None:
94
98
  repl_tool = get_default_repl_tool()
95
- if function_signatures is not None:
99
+ if function_signatures:
96
100
  insert_callables_into_global(function_signatures=function_signatures, repl_tool=repl_tool)
97
101
  output = str(repl_tool.invoke(code, config=config, **kwargs)) # pyright: ignore[reportUnknownMemberType]
98
102
  return cls(code=code, output=output)
@@ -111,7 +115,7 @@ class CodeExecutionResult(NamedTuple):
111
115
  """
112
116
  if repl_tool is None:
113
117
  repl_tool = get_default_repl_tool()
114
- if function_signatures is not None:
118
+ if function_signatures:
115
119
  insert_callables_into_global(function_signatures=function_signatures, repl_tool=repl_tool)
116
120
  output = str(await repl_tool.ainvoke(code, config=config, **kwargs)) # pyright: ignore[reportUnknownMemberType]
117
121
  return cls(code=code, output=output)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatterer
3
- Version: 0.1.9
3
+ Version: 0.1.10
4
4
  Summary: The highest-level interface for various LLM APIs.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -18,6 +18,7 @@ Requires-Dist: pillow>=11.1.0; extra == "conversion"
18
18
  Requires-Dist: mistune>=3.1.2; extra == "conversion"
19
19
  Requires-Dist: markitdown>=0.0.2; extra == "conversion"
20
20
  Requires-Dist: pymupdf>=1.25.4; extra == "conversion"
21
+ Requires-Dist: youtube-transcript-api>=1.0.2; extra == "conversion"
21
22
  Provides-Extra: langchain
22
23
  Requires-Dist: chatterer[langchain-providers]; extra == "langchain"
23
24
  Requires-Dist: langchain-experimental>=0.3.4; extra == "langchain"
@@ -1,12 +1,13 @@
1
- chatterer/__init__.py,sha256=QqW6ITeJ7Qpt42BvVzTgI8M6vKRYIlwSDa0nMO4NsV0,1209
2
- chatterer/language_model.py,sha256=J7_iLtfjr-0tNTejrY4_vLiEWGTnRGGUb_x9G2CZ-Vg,20083
1
+ chatterer/__init__.py,sha256=BPgCQ6VWGBXSh8xJr_0bpM0hcOOUz0KoxcKxOd9GYyI,1388
2
+ chatterer/language_model.py,sha256=qnVC5_W4IYM0y0o1PTYMGXUlblRv5fsRk0zIiL_vT3Q,24491
3
3
  chatterer/messages.py,sha256=OtbZ3two0LUQ4PXES97FDIBUSO3IcMHdFV1VFkDL2mI,229
4
4
  chatterer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  chatterer/strategies/__init__.py,sha256=SdOggbmHpw4f7Njwy-T8q64e91OLOUp1k0a0ozZd4qI,221
6
6
  chatterer/strategies/atom_of_thoughts.py,sha256=CygOCLu5vLk-fzY9O-iE3qLShfjD7iY40ks9jH4ULBM,40872
7
7
  chatterer/strategies/base.py,sha256=b2gMPqodp97OP1dkHfj0UqixjdjVhmTw_V5qJ7i2S6g,427
8
- chatterer/tools/__init__.py,sha256=XomZMXHKhMyLprQkCfAKetI_uueHH184xWESjTKJeeA,560
8
+ chatterer/tools/__init__.py,sha256=hmWIuLJWotGQodL__i4LLbHdXe7Nl5uKHqNke9tHMro,705
9
9
  chatterer/tools/convert_to_text.py,sha256=kBqxCJ0IoiAw2eiPYqep_SPZm-TtYKF7mdACLsWQUuI,15915
10
+ chatterer/tools/youtube.py,sha256=5hwASZWA92d_7Y5RqlCK2tULRBQx8bGnE_0NCnvaKi0,5499
10
11
  chatterer/tools/citation_chunking/__init__.py,sha256=gG7Fnkkp28UpcWMbfMY_4gqzZSZ8QzlhalHBoeoq7K0,82
11
12
  chatterer/tools/citation_chunking/chunks.py,sha256=50Dpa43RaYftlNox8tM1qI8htZ3_AJ9Uyyn02WsmxYk,2173
12
13
  chatterer/tools/citation_chunking/citation_chunker.py,sha256=yx5O9pUkowlNcFyyNf7f3sbq7-CV8AXOzFnviDldPR8,4894
@@ -18,9 +19,9 @@ chatterer/tools/webpage_to_markdown/__init__.py,sha256=bHH4qfnXyw8Zz-yBPLaTezF1s
18
19
  chatterer/tools/webpage_to_markdown/playwright_bot.py,sha256=yP0KixYZNQ4Kn_ZCFDI3mVyBD_DpUGfqgklpaGJUTCU,27496
19
20
  chatterer/tools/webpage_to_markdown/utils.py,sha256=ZLUU94imYciEdynD2K7Dmcsbt8BVQTaOP56Ba6DAFvk,12593
20
21
  chatterer/utils/__init__.py,sha256=8nzpFJKU_wSRPH6LBP6HRBotPMrSl_VO9UlmFprTrK0,334
21
- chatterer/utils/code_agent.py,sha256=phMMXHeZNkzHrySX72y50IW3-o2MOSriPV9IUPQd4nU,4973
22
+ chatterer/utils/code_agent.py,sha256=UaWdeGzJMPzRSFy9yrxuveBJsvOPSa0te6OuE18bees,5143
22
23
  chatterer/utils/image.py,sha256=F3_D1677UDFlgp-UQBS_ChkNODzf_VOfjYNSUi02MaI,10852
23
- chatterer-0.1.9.dist-info/METADATA,sha256=GbjuJgbQJ09TEYt7lvI96Od5saTZ_y0KDzmybsW-0H0,4388
24
- chatterer-0.1.9.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
25
- chatterer-0.1.9.dist-info/top_level.txt,sha256=7nSQKP0bHxPRc7HyzdbKsJdkvPgYD0214o6slRizv9s,10
26
- chatterer-0.1.9.dist-info/RECORD,,
24
+ chatterer-0.1.10.dist-info/METADATA,sha256=qPx7b41yUvBG0XFH4ra89LIyOSUZUo_8gZ-adVkTKME,4458
25
+ chatterer-0.1.10.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
26
+ chatterer-0.1.10.dist-info/top_level.txt,sha256=7nSQKP0bHxPRc7HyzdbKsJdkvPgYD0214o6slRizv9s,10
27
+ chatterer-0.1.10.dist-info/RECORD,,