chatterer 0.1.14__py3-none-any.whl → 0.1.17__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
@@ -41,9 +41,7 @@ from .tools import (
41
41
  render_pdf_as_image,
42
42
  )
43
43
  from .utils import (
44
- ArgumentSpec,
45
44
  Base64Image,
46
- BaseArguments,
47
45
  CodeExecutionResult,
48
46
  FunctionSignature,
49
47
  get_default_repl_tool,
@@ -92,6 +90,4 @@ __all__ = [
92
90
  "extract_text_from_pdf",
93
91
  "open_pdf",
94
92
  "render_pdf_as_image",
95
- "ArgumentSpec",
96
- "BaseArguments",
97
93
  ]
chatterer/interactive.py CHANGED
@@ -1,5 +1,5 @@
1
- import sys
2
- from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
1
+ from functools import cached_property
2
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, Type, TypeVar, cast, overload
3
3
 
4
4
  from langchain_core.messages import (
5
5
  AIMessage,
@@ -9,8 +9,11 @@ from langchain_core.messages import (
9
9
  )
10
10
  from langchain_core.runnables import RunnableConfig
11
11
  from pydantic import BaseModel, Field
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.prompt import Prompt
12
15
 
13
- from .language_model import Chatterer
16
+ from .language_model import Chatterer, LanguageModelInput
14
17
  from .utils.code_agent import (
15
18
  DEFAULT_CODE_GENERATION_PROMPT,
16
19
  DEFAULT_FUNCTION_REFERENCE_PREFIX_PROMPT,
@@ -25,6 +28,7 @@ if TYPE_CHECKING:
25
28
  # Import only for type hinting to avoid circular dependencies if necessary
26
29
  from langchain_experimental.tools.python.tool import PythonAstREPLTool
27
30
 
31
+ T = TypeVar("T", bound=BaseModel)
28
32
 
29
33
  # --- Pydantic Models ---
30
34
 
@@ -94,9 +98,8 @@ class Think(BaseModel):
94
98
 
95
99
  # --- Interactive Shell Function ---
96
100
 
97
-
98
101
  def interactive_shell(
99
- chatterer: Chatterer = Chatterer.openai(), # Assuming Chatterer.openai() is correct
102
+ chatterer: Chatterer,
100
103
  system_instruction: BaseMessage | Iterable[BaseMessage] = ([
101
104
  SystemMessage(
102
105
  "You are an AI assistant capable of answering questions and executing Python code to help users solve tasks."
@@ -112,10 +115,6 @@ def interactive_shell(
112
115
  **kwargs: Any,
113
116
  ) -> None:
114
117
  try:
115
- from rich.console import Console
116
- from rich.panel import Panel
117
- from rich.prompt import Prompt
118
-
119
118
  console = Console()
120
119
  # Style settings
121
120
  AI_STYLE = "bold bright_blue"
@@ -125,23 +124,28 @@ def interactive_shell(
125
124
  except ImportError:
126
125
  raise ImportError("Rich library not found. Please install it: pip install rich")
127
126
 
127
+ # --- Shell Initialization and Main Loop ---
128
+ if repl_tool is None:
129
+ repl_tool = get_default_repl_tool()
130
+
131
+ def set_locals(**kwargs: object) -> None:
132
+ """Set local variables for the REPL tool."""
133
+ if repl_tool.locals is None: # pyright: ignore[reportUnknownMemberType]
134
+ repl_tool.locals = {}
135
+ for key, value in kwargs.items():
136
+ repl_tool.locals[key] = value # pyright: ignore[reportUnknownMemberType]
137
+
128
138
  def respond(messages: list[BaseMessage]) -> str:
129
139
  response = ""
130
- if "rich" not in sys.modules:
140
+ with console.status("[bold yellow]AI is thinking..."):
141
+ response_panel = Panel("", title="AI Response", style=AI_STYLE, border_style="blue")
142
+ current_content = ""
131
143
  for chunk in chatterer.generate_stream(messages=messages):
132
- print(chunk, end="", flush=True)
133
- response += chunk
134
- print()
135
- else:
136
- with console.status("[bold yellow]AI is thinking..."):
137
- response_panel = Panel("", title="AI Response", style=AI_STYLE, border_style="blue")
138
- current_content = ""
139
- for chunk in chatterer.generate_stream(messages=messages):
140
- current_content += chunk
141
- # Update renderable (might not display smoothly without Live)
142
- response_panel.renderable = current_content
143
- response = current_content
144
- console.print(Panel(response, title="AI Response", style=AI_STYLE))
144
+ current_content += chunk
145
+ # Update renderable (might not display smoothly without Live)
146
+ response_panel.renderable = current_content
147
+ response = current_content
148
+ console.print(Panel(response, title="AI Response", style=AI_STYLE))
145
149
  return response.strip()
146
150
 
147
151
  def complete_task(think_before_speak: ThinkBeforeSpeak) -> None:
@@ -159,15 +163,15 @@ def interactive_shell(
159
163
 
160
164
  while True:
161
165
  current_context = context + session_messages
162
- is_tool_call_needed: IsToolCallNeeded = chatterer.generate_pydantic(
163
- response_model=IsToolCallNeeded,
164
- messages=augment_prompt_for_toolcall(
166
+ is_tool_call_needed: IsToolCallNeeded = chatterer(
167
+ augment_prompt_for_toolcall(
165
168
  function_signatures=function_signatures,
166
169
  messages=current_context,
167
170
  prompt_for_code_invoke=prompt_for_code_invoke,
168
171
  function_reference_prefix=function_reference_prefix,
169
172
  function_reference_seperator=function_reference_seperator,
170
173
  ),
174
+ IsToolCallNeeded,
171
175
  config=config,
172
176
  stop=stop,
173
177
  **kwargs,
@@ -175,7 +179,8 @@ def interactive_shell(
175
179
 
176
180
  if is_tool_call_needed.is_tool_call_needed:
177
181
  # --- Code Execution Path ---
178
- code_execution: CodeExecutionResult = chatterer.invoke_code_execution(
182
+ set_locals(__context__=context, __session__=session_messages)
183
+ code_execution: CodeExecutionResult = chatterer.exec(
179
184
  messages=current_context,
180
185
  repl_tool=repl_tool,
181
186
  prompt_for_code_invoke=prompt_for_code_invoke,
@@ -200,15 +205,15 @@ def interactive_shell(
200
205
 
201
206
  # --- Review Code Execution ---
202
207
  current_context_after_exec = context + session_messages
203
- decision = chatterer.generate_pydantic(
204
- response_model=ReviewOnToolcall,
205
- messages=augment_prompt_for_toolcall(
208
+ decision = chatterer(
209
+ augment_prompt_for_toolcall(
206
210
  function_signatures=function_signatures,
207
211
  messages=current_context_after_exec,
208
212
  prompt_for_code_invoke=prompt_for_code_invoke,
209
213
  function_reference_prefix=function_reference_prefix,
210
214
  function_reference_seperator=function_reference_seperator,
211
215
  ),
216
+ ReviewOnToolcall,
212
217
  config=config,
213
218
  stop=stop,
214
219
  **kwargs,
@@ -233,15 +238,15 @@ def interactive_shell(
233
238
  else:
234
239
  # --- Thinking Path (No Code Needed) ---
235
240
  current_context_before_think = context + session_messages
236
- decision = chatterer.generate_pydantic(
237
- response_model=Think, # Uses updated description
238
- messages=augment_prompt_for_toolcall(
241
+ decision = chatterer(
242
+ augment_prompt_for_toolcall(
239
243
  function_signatures=function_signatures,
240
244
  messages=current_context_before_think,
241
245
  prompt_for_code_invoke=prompt_for_code_invoke,
242
246
  function_reference_prefix=function_reference_prefix,
243
247
  function_reference_seperator=function_reference_seperator,
244
248
  ),
249
+ Think,
245
250
  config=config,
246
251
  stop=stop,
247
252
  **kwargs,
@@ -277,10 +282,6 @@ def interactive_shell(
277
282
  # Add the final AI response to the main context
278
283
  context.append(AIMessage(content=response))
279
284
 
280
- # --- Shell Initialization and Main Loop ---
281
- if repl_tool is None:
282
- repl_tool = get_default_repl_tool()
283
-
284
285
  if additional_callables:
285
286
  if callable(additional_callables):
286
287
  additional_callables = [additional_callables]
@@ -321,15 +322,15 @@ def interactive_shell(
321
322
 
322
323
  try:
323
324
  # Initial planning step
324
- initial_plan_decision = chatterer.generate_pydantic(
325
- response_model=ThinkBeforeSpeak,
326
- messages=augment_prompt_for_toolcall(
325
+ initial_plan_decision = chatterer(
326
+ augment_prompt_for_toolcall(
327
327
  function_signatures=function_signatures,
328
328
  messages=context,
329
329
  prompt_for_code_invoke=prompt_for_code_invoke,
330
330
  function_reference_prefix=function_reference_prefix,
331
331
  function_reference_seperator=function_reference_seperator,
332
332
  ),
333
+ ThinkBeforeSpeak,
333
334
  config=config,
334
335
  stop=stop,
335
336
  **kwargs,
@@ -350,4 +351,4 @@ def interactive_shell(
350
351
 
351
352
 
352
353
  if __name__ == "__main__":
353
- interactive_shell()
354
+ interactive_shell(chatterer=Chatterer.openai())
@@ -6,6 +6,7 @@ from typing import (
6
6
  Callable,
7
7
  Iterable,
8
8
  Iterator,
9
+ Literal,
9
10
  Optional,
10
11
  Self,
11
12
  Sequence,
@@ -20,7 +21,7 @@ from langchain_core.language_models.chat_models import BaseChatModel
20
21
  from langchain_core.runnables.base import Runnable
21
22
  from langchain_core.runnables.config import RunnableConfig
22
23
  from langchain_core.utils.utils import secret_from_env
23
- from pydantic import BaseModel, Field
24
+ from pydantic import BaseModel, Field, SecretStr
24
25
 
25
26
  from .messages import AIMessage, BaseMessage, HumanMessage, UsageMetadata
26
27
  from .utils.code_agent import CodeExecutionResult, FunctionSignature, augment_prompt_for_toolcall
@@ -65,57 +66,80 @@ class Chatterer(BaseModel):
65
66
 
66
67
  @classmethod
67
68
  def from_provider(
68
- cls,
69
- provider_and_model: str,
70
- structured_output_kwargs: Optional[dict[str, Any]] = {"strict": True},
69
+ cls, provider_and_model: str, structured_output_kwargs: Optional[dict[str, object]] = {"strict": True}
71
70
  ) -> Self:
72
71
  backend, model = provider_and_model.split(":", 1)
73
- if backend == "openai":
74
- return cls.openai(model=model, structured_output_kwargs=structured_output_kwargs)
75
- elif backend == "anthropic":
76
- return cls.anthropic(model_name=model, structured_output_kwargs=structured_output_kwargs)
77
- elif backend == "google":
78
- return cls.google(model=model, structured_output_kwargs=structured_output_kwargs)
79
- elif backend == "ollama":
80
- return cls.ollama(model=model, structured_output_kwargs=structured_output_kwargs)
81
- elif backend == "openrouter":
82
- return cls.open_router(model=model, structured_output_kwargs=structured_output_kwargs)
72
+ backends = cls.get_backends()
73
+ if func := backends.get(backend):
74
+ return func(model, structured_output_kwargs)
83
75
  else:
84
- raise ValueError(f"Unsupported backend model: {backend}")
76
+ raise ValueError(f"Unsupported provider: {backend}. Supported providers are: {', '.join(backends.keys())}.")
77
+
78
+ @classmethod
79
+ def get_backends(cls) -> dict[str, Callable[[str, Optional[dict[str, object]]], Self]]:
80
+ return {
81
+ "openai": cls.openai,
82
+ "anthropic": cls.anthropic,
83
+ "google": cls.google,
84
+ "ollama": cls.ollama,
85
+ "openrouter": cls.open_router,
86
+ "xai": cls.xai,
87
+ }
85
88
 
86
89
  @classmethod
87
90
  def openai(
88
91
  cls,
89
- model: str = "gpt-4o-mini",
90
- structured_output_kwargs: Optional[dict[str, Any]] = {"strict": True},
92
+ model: str = "gpt-4.1",
93
+ structured_output_kwargs: Optional[dict[str, object]] = {"strict": True},
94
+ api_key: Optional[str] = None,
95
+ **kwargs: Any,
91
96
  ) -> Self:
92
97
  from langchain_openai import ChatOpenAI
93
98
 
94
- return cls(client=ChatOpenAI(model=model), structured_output_kwargs=structured_output_kwargs or {})
99
+ return cls(
100
+ client=ChatOpenAI(
101
+ model=model,
102
+ api_key=_get_api_key(api_key=api_key, env_key="OPENAI_API_KEY", raise_if_none=False),
103
+ **kwargs,
104
+ ),
105
+ structured_output_kwargs=structured_output_kwargs or {},
106
+ )
95
107
 
96
108
  @classmethod
97
109
  def anthropic(
98
110
  cls,
99
111
  model_name: str = "claude-3-7-sonnet-20250219",
100
- structured_output_kwargs: Optional[dict[str, Any]] = None,
112
+ structured_output_kwargs: Optional[dict[str, object]] = None,
113
+ api_key: Optional[str] = None,
114
+ **kwargs: Any,
101
115
  ) -> Self:
102
116
  from langchain_anthropic import ChatAnthropic
103
117
 
104
118
  return cls(
105
- client=ChatAnthropic(model_name=model_name, timeout=None, stop=None),
119
+ client=ChatAnthropic(
120
+ model_name=model_name,
121
+ api_key=_get_api_key(api_key=api_key, env_key="ANTHROPIC_API_KEY", raise_if_none=True),
122
+ **kwargs,
123
+ ),
106
124
  structured_output_kwargs=structured_output_kwargs or {},
107
125
  )
108
126
 
109
127
  @classmethod
110
128
  def google(
111
129
  cls,
112
- model: str = "gemini-2.0-flash",
113
- structured_output_kwargs: Optional[dict[str, Any]] = None,
130
+ model: str = "gemini-2.5-flash-preview-04-17",
131
+ structured_output_kwargs: Optional[dict[str, object]] = None,
132
+ api_key: Optional[str] = None,
133
+ **kwargs: Any,
114
134
  ) -> Self:
115
135
  from langchain_google_genai import ChatGoogleGenerativeAI
116
136
 
117
137
  return cls(
118
- client=ChatGoogleGenerativeAI(model=model),
138
+ client=ChatGoogleGenerativeAI(
139
+ model=model,
140
+ api_key=_get_api_key(api_key=api_key, env_key="GOOGLE_API_KEY", raise_if_none=False),
141
+ **kwargs,
142
+ ),
119
143
  structured_output_kwargs=structured_output_kwargs or {},
120
144
  )
121
145
 
@@ -123,12 +147,13 @@ class Chatterer(BaseModel):
123
147
  def ollama(
124
148
  cls,
125
149
  model: str = "deepseek-r1:1.5b",
126
- structured_output_kwargs: Optional[dict[str, Any]] = None,
150
+ structured_output_kwargs: Optional[dict[str, object]] = None,
151
+ **kwargs: Any,
127
152
  ) -> Self:
128
153
  from langchain_ollama import ChatOllama
129
154
 
130
155
  return cls(
131
- client=ChatOllama(model=model),
156
+ client=ChatOllama(model=model, **kwargs),
132
157
  structured_output_kwargs=structured_output_kwargs or {},
133
158
  )
134
159
 
@@ -136,7 +161,9 @@ class Chatterer(BaseModel):
136
161
  def open_router(
137
162
  cls,
138
163
  model: str = "openrouter/quasar-alpha",
139
- structured_output_kwargs: Optional[dict[str, Any]] = None,
164
+ structured_output_kwargs: Optional[dict[str, object]] = None,
165
+ api_key: Optional[str] = None,
166
+ **kwargs: Any,
140
167
  ) -> Self:
141
168
  from langchain_openai import ChatOpenAI
142
169
 
@@ -144,7 +171,29 @@ class Chatterer(BaseModel):
144
171
  client=ChatOpenAI(
145
172
  model=model,
146
173
  base_url="https://openrouter.ai/api/v1",
147
- api_key=secret_from_env("OPENROUTER_API_KEY", default=None)(),
174
+ api_key=_get_api_key(api_key=api_key, env_key="OPENROUTER_API_KEY", raise_if_none=False),
175
+ **kwargs,
176
+ ),
177
+ structured_output_kwargs=structured_output_kwargs or {},
178
+ )
179
+
180
+ @classmethod
181
+ def xai(
182
+ cls,
183
+ model: str = "grok-3-mini",
184
+ structured_output_kwargs: Optional[dict[str, object]] = None,
185
+ base_url: str = "https://api.x.ai/v1",
186
+ api_key: Optional[str] = None,
187
+ **kwargs: Any,
188
+ ) -> Self:
189
+ from langchain_openai import ChatOpenAI
190
+
191
+ return cls(
192
+ client=ChatOpenAI(
193
+ model=model,
194
+ base_url=base_url,
195
+ api_key=_get_api_key(api_key=api_key, env_key="XAI_API_KEY", raise_if_none=False),
196
+ **kwargs,
148
197
  ),
149
198
  structured_output_kwargs=structured_output_kwargs or {},
150
199
  )
@@ -367,7 +416,7 @@ class Chatterer(BaseModel):
367
416
  "total_tokens": approx_tokens,
368
417
  }
369
418
 
370
- def invoke_code_execution(
419
+ def exec(
371
420
  self,
372
421
  messages: LanguageModelInput,
373
422
  repl_tool: Optional["PythonAstREPLTool"] = None,
@@ -401,7 +450,12 @@ class Chatterer(BaseModel):
401
450
  **kwargs,
402
451
  )
403
452
 
404
- async def ainvoke_code_execution(
453
+ @property
454
+ def invoke_code_execution(self) -> Callable[..., CodeExecutionResult]:
455
+ """Alias for exec method for backward compatibility."""
456
+ return self.exec
457
+
458
+ async def aexec(
405
459
  self,
406
460
  messages: LanguageModelInput,
407
461
  repl_tool: Optional["PythonAstREPLTool"] = None,
@@ -432,6 +486,11 @@ class Chatterer(BaseModel):
432
486
  **kwargs,
433
487
  )
434
488
 
489
+ @property
490
+ def ainvoke_code_execution(self):
491
+ """Alias for aexec method for backward compatibility."""
492
+ return self.aexec
493
+
435
494
 
436
495
  class PythonCodeToExecute(BaseModel):
437
496
  code: str = Field(description="Python code to execute")
@@ -452,3 +511,23 @@ def _with_structured_output(
452
511
  structured_output_kwargs: dict[str, Any],
453
512
  ) -> Runnable[LanguageModelInput, dict[object, object] | BaseModel]:
454
513
  return client.with_structured_output(schema=response_model, **structured_output_kwargs) # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
514
+
515
+
516
+ @overload
517
+ def _get_api_key(api_key: Optional[str], env_key: str, raise_if_none: Literal[True]) -> SecretStr: ...
518
+ @overload
519
+ def _get_api_key(api_key: Optional[str], env_key: str, raise_if_none: Literal[False]) -> Optional[SecretStr]: ...
520
+ def _get_api_key(api_key: Optional[str], env_key: str, raise_if_none: bool) -> Optional[SecretStr]:
521
+ if api_key is None:
522
+ api_key_found: SecretStr | None = secret_from_env(env_key, default=None)()
523
+ if raise_if_none and api_key_found is None:
524
+ raise ValueError(
525
+ (
526
+ f"Did not find API key, please add an environment variable"
527
+ f" `{env_key}` which contains it, or pass"
528
+ f" api_key as a named parameter."
529
+ )
530
+ )
531
+ return api_key_found
532
+ else:
533
+ return SecretStr(api_key)
@@ -1,5 +1,4 @@
1
1
  from .base64_image import Base64Image
2
- from .cli import ArgumentSpec, BaseArguments
3
2
  from .code_agent import (
4
3
  CodeExecutionResult,
5
4
  FunctionSignature,
@@ -13,6 +12,4 @@ __all__ = [
13
12
  "CodeExecutionResult",
14
13
  "get_default_repl_tool",
15
14
  "insert_callables_into_global",
16
- "BaseArguments",
17
- "ArgumentSpec",
18
15
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatterer
3
- Version: 0.1.14
3
+ Version: 0.1.17
4
4
  Summary: The highest-level interface for various LLM APIs.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -9,15 +9,18 @@ Requires-Dist: langchain>=0.3.19
9
9
  Requires-Dist: langchain-openai>=0.3.11
10
10
  Requires-Dist: pillow>=11.1.0
11
11
  Requires-Dist: regex>=2024.11.6
12
+ Requires-Dist: rich>=13.9.4
12
13
  Provides-Extra: dev
13
14
  Requires-Dist: neo4j-extension>=0.1.14; extra == "dev"
14
15
  Requires-Dist: colorama>=0.4.6; extra == "dev"
15
16
  Requires-Dist: ipykernel>=6.29.5; extra == "dev"
17
+ Requires-Dist: spargear>=0.1.4; extra == "dev"
16
18
  Provides-Extra: conversion
17
19
  Requires-Dist: youtube-transcript-api>=1.0.3; extra == "conversion"
18
20
  Requires-Dist: chatterer[browser]; extra == "conversion"
19
21
  Requires-Dist: chatterer[pdf]; extra == "conversion"
20
22
  Requires-Dist: chatterer[markdown]; extra == "conversion"
23
+ Requires-Dist: chatterer[video]; extra == "conversion"
21
24
  Provides-Extra: browser
22
25
  Requires-Dist: playwright>=1.50.0; extra == "browser"
23
26
  Provides-Extra: pdf
@@ -28,6 +31,8 @@ Requires-Dist: markitdown[all]>=0.1.1; extra == "markdown"
28
31
  Requires-Dist: markdownify>=1.1.0; extra == "markdown"
29
32
  Requires-Dist: commonmark>=0.9.1; extra == "markdown"
30
33
  Requires-Dist: mistune>=3.1.3; extra == "markdown"
34
+ Provides-Extra: video
35
+ Requires-Dist: pydub>=0.25.1; extra == "video"
31
36
  Provides-Extra: langchain
32
37
  Requires-Dist: chatterer[langchain-providers]; extra == "langchain"
33
38
  Requires-Dist: langchain-experimental>=0.3.4; extra == "langchain"
@@ -1,6 +1,6 @@
1
- chatterer/__init__.py,sha256=Mxt_qGUyLEbbJ3VU-qr1EBgE2tSNFvqxiL-1QcXLu2c,2252
2
- chatterer/interactive.py,sha256=4_XKcdpRNLWEbEXwszJG-sECGun7_U1tEhd30brUPZM,16784
3
- chatterer/language_model.py,sha256=L3mVbbtiRoUx1UfJy0gCJ4laf03nyfDiDQtbEcB5Nv0,17594
1
+ chatterer/__init__.py,sha256=KmoWOad8UgqYeBCYXhFeeXCL9lzaB-Aa3KVqoAaXrMg,2174
2
+ chatterer/interactive.py,sha256=JTtIUbG_ABqmSKyQT_0vkq_K09ZCv4nyr_pAEMqhk88,16780
3
+ chatterer/language_model.py,sha256=j4MBUeHtifbufXyFUpKnxTlMbCwRQSgIgGmL09M4ifE,20126
4
4
  chatterer/messages.py,sha256=SIvG9hMHaPG4AFadeRbj5yPnMq2J06fHA4D8jrkz4kQ,458
5
5
  chatterer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  chatterer/common_types/__init__.py,sha256=Ais0kqzQJj4sED24xdv5azIxHkkj0vXWb4LC41dFkBQ,355
@@ -22,13 +22,12 @@ chatterer/tools/citation_chunking/citations.py,sha256=BWhSwzZccvu0Db-OxEbsuEGEz-
22
22
  chatterer/tools/citation_chunking/prompt.py,sha256=so-8uFQ5b2Zq2V5Brfxd76bEnKYkHovYsohAnbxWEnY,7557
23
23
  chatterer/tools/citation_chunking/reference.py,sha256=m47XYaB5uFff_x_k7US9hNr-SpZjKnl-GuzsGaQzcZo,893
24
24
  chatterer/tools/citation_chunking/utils.py,sha256=Xytm9lMrS783Po1qWAdEJ8q7Q3l2UMzwHd9EkYTRiwk,6210
25
- chatterer/utils/__init__.py,sha256=wA_nd1si3MbypA4biQRa3MYBqkHwTSCioYgaHce42Ls,412
25
+ chatterer/utils/__init__.py,sha256=of2NeLOjsAI79TgA4bL7UggCnAc7xT9eu3eeUBt9K8k,326
26
26
  chatterer/utils/base64_image.py,sha256=w5SQoHVPDEQtFSUhV1l9GKg-IQy1WK5nyjv9k0upow8,10839
27
27
  chatterer/utils/bytesio.py,sha256=QabdJCZsabPaiYVfJcdXzdiHjuhqDlz1vJuLJ60P7TY,2559
28
- chatterer/utils/cli.py,sha256=F-Ooz4_pFeX4Y36R7OGhNrE7-IDyVIv2EEelYwJ4H0Y,19392
29
28
  chatterer/utils/code_agent.py,sha256=3Wu0M6VvfvWKu4UUoJ0rV-WjvLj06r15_01vft3d1Ns,10231
30
29
  chatterer/utils/imghdr.py,sha256=aZ1_AsRzyTsbV7uoeAZMVaC-hj73kvnFHMdHtFKskdE,3694
31
- chatterer-0.1.14.dist-info/METADATA,sha256=nMitUtZcmWPpmwVl_MTuzvK_AK-t6CuFlsdIxiSmbfA,11267
32
- chatterer-0.1.14.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
33
- chatterer-0.1.14.dist-info/top_level.txt,sha256=7nSQKP0bHxPRc7HyzdbKsJdkvPgYD0214o6slRizv9s,10
34
- chatterer-0.1.14.dist-info/RECORD,,
30
+ chatterer-0.1.17.dist-info/METADATA,sha256=tz1IPuxw7BSJR4N5SpT9FFE5Y9qE9twW9Hoxw18XF1I,11466
31
+ chatterer-0.1.17.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
32
+ chatterer-0.1.17.dist-info/top_level.txt,sha256=7nSQKP0bHxPRc7HyzdbKsJdkvPgYD0214o6slRizv9s,10
33
+ chatterer-0.1.17.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
chatterer/utils/cli.py DELETED
@@ -1,476 +0,0 @@
1
- import argparse
2
- import io
3
- import typing
4
- import warnings
5
- from dataclasses import dataclass, field, fields
6
- from typing import (
7
- IO,
8
- Callable,
9
- Dict,
10
- Generic,
11
- Iterable,
12
- List,
13
- Literal,
14
- NamedTuple,
15
- Optional,
16
- Sequence,
17
- Tuple,
18
- TypeVar,
19
- Union,
20
- )
21
-
22
- # --- Type Definitions ---
23
- SUPPRESS_LITERAL_TYPE = Literal["==SUPPRESS=="]
24
- SUPPRESS: SUPPRESS_LITERAL_TYPE = "==SUPPRESS=="
25
- ACTION_TYPES_THAT_DONT_SUPPORT_TYPE_KWARG = (
26
- "store_const",
27
- "store_true",
28
- "store_false",
29
- "append_const",
30
- "count",
31
- "help",
32
- "version",
33
- )
34
- Action = Optional[
35
- Literal[
36
- "store",
37
- "store_const",
38
- "store_true",
39
- "store_false",
40
- "append",
41
- "append_const",
42
- "count",
43
- "help",
44
- "version",
45
- "extend",
46
- ]
47
- ]
48
- T = TypeVar("T")
49
-
50
-
51
- @dataclass
52
- class ArgumentSpec(Generic[T]):
53
- """Represents the specification for a command-line argument."""
54
-
55
- name_or_flags: List[str]
56
- action: Action = None
57
- nargs: Optional[Union[int, Literal["*", "+", "?"]]] = None
58
- const: Optional[object] = None
59
- default: Optional[Union[T, SUPPRESS_LITERAL_TYPE]] = None
60
- choices: Optional[Sequence[T]] = None
61
- required: bool = False
62
- help: str = ""
63
- metavar: Optional[str] = None
64
- version: Optional[str] = None
65
- type: Optional[Union[Callable[[str], T], argparse.FileType]] = None
66
- value: Optional[T] = field(init=False, default=None) # Parsed value stored here
67
-
68
- @property
69
- def value_not_none(self) -> T:
70
- """Returns the value, raising an error if it's None."""
71
- if self.value is None:
72
- raise ValueError(f"Value for {self.name_or_flags} is None.")
73
- return self.value
74
-
75
- def get_add_argument_kwargs(self) -> Dict[str, object]:
76
- """Prepares keyword arguments for argparse.ArgumentParser.add_argument."""
77
- kwargs: Dict[str, object] = {}
78
- argparse_fields: set[str] = {f.name for f in fields(self) if f.name not in ("name_or_flags", "value")}
79
- for field_name in argparse_fields:
80
- attr_value = getattr(self, field_name)
81
- if field_name == "default":
82
- if attr_value is None:
83
- pass # Keep default=None if explicitly set or inferred
84
- elif attr_value in get_args(SUPPRESS_LITERAL_TYPE):
85
- kwargs[field_name] = argparse.SUPPRESS
86
- else:
87
- kwargs[field_name] = attr_value
88
- elif attr_value is not None:
89
- if field_name == "type" and self.action in ACTION_TYPES_THAT_DONT_SUPPORT_TYPE_KWARG:
90
- continue
91
- kwargs[field_name] = attr_value
92
- return kwargs
93
-
94
-
95
- class ArgumentSpecType(NamedTuple):
96
- T: object # The T in ArgumentSpec[T]
97
- element_type: typing.Optional[typing.Type[object]] # The E in ArgumentSpec[List[E]] or ArgumentSpec[Tuple[E]]
98
-
99
- @classmethod
100
- def from_hint(cls, hints: Dict[str, object], attr_name: str):
101
- if (
102
- (hint := hints.get(attr_name))
103
- and (hint_origin := get_origin(hint))
104
- and (hint_args := get_args(hint))
105
- and isinstance(hint_origin, type)
106
- and issubclass(hint_origin, ArgumentSpec)
107
- ):
108
- T: object = hint_args[0] # Extract T
109
- element_type: typing.Optional[object] = None
110
- if isinstance(outer_origin := get_origin(T), type):
111
- if issubclass(outer_origin, list) and (args := get_args(T)):
112
- element_type = args[0] # Extract E
113
- elif issubclass(outer_origin, tuple) and (args := get_args(T)):
114
- element_type = args # Extract E
115
- else:
116
- element_type = None # The E in ArgumentSpec[List[E]] or Tuple[E, ...]
117
- if not isinstance(element_type, type):
118
- element_type = None
119
- return cls(T=T, element_type=element_type)
120
-
121
- @property
122
- def choices(self) -> typing.Optional[Tuple[object, ...]]:
123
- # ArgumentSpec[Literal["A", "B"]] or ArgumentSpec[List[Literal["A", "B"]]]
124
- T_origin = get_origin(self.T)
125
- if (
126
- isinstance(T_origin, type)
127
- and (issubclass(T_origin, (list, tuple)))
128
- and (args := get_args(self.T))
129
- and (get_origin(arg := args[0]) is typing.Literal)
130
- and (literals := get_args(arg))
131
- ):
132
- return literals
133
- elif T_origin is typing.Literal and (args := get_args(self.T)):
134
- return args
135
-
136
- @property
137
- def type(self) -> typing.Optional[typing.Type[object]]:
138
- if self.element_type is not None:
139
- return self.element_type # If it's List[E] or Sequence[E], use E as type
140
- elif self.T and isinstance(self.T, type):
141
- return self.T # Use T as type
142
-
143
- @property
144
- def should_return_as_list(self) -> bool:
145
- """Determines if the argument should be returned as a list."""
146
- T_origin = get_origin(self.T)
147
- if isinstance(T_origin, type):
148
- if issubclass(T_origin, list):
149
- return True
150
- return False
151
-
152
- @property
153
- def should_return_as_tuple(self) -> bool:
154
- """Determines if the argument should be returned as a tuple."""
155
- T_origin = get_origin(self.T)
156
- if isinstance(T_origin, type):
157
- if issubclass(T_origin, tuple):
158
- return True
159
- return False
160
-
161
- @property
162
- def tuple_nargs(self) -> Optional[int]:
163
- if self.should_return_as_tuple and (args := get_args(self.T)) and Ellipsis not in args:
164
- return len(args)
165
-
166
-
167
- class BaseArguments:
168
- """Base class for defining arguments declaratively using ArgumentSpec."""
169
-
170
- __argspec__: Dict[str, ArgumentSpec[object]]
171
- __argspectype__: Dict[str, ArgumentSpecType]
172
-
173
- def __init_subclass__(cls, **kwargs: object) -> None:
174
- """
175
- Processes ArgumentSpec definitions in subclasses upon class creation.
176
- Automatically infers 'type' and 'choices' from type hints if possible.
177
- """
178
- super().__init_subclass__(**kwargs)
179
- cls.__argspec__ = {}
180
- cls.__argspectype__ = {}
181
- for current_cls in reversed(cls.__mro__):
182
- if current_cls is object or current_cls is BaseArguments:
183
- continue
184
- current_vars = vars(current_cls)
185
- try:
186
- hints: Dict[str, object] = typing.get_type_hints(current_cls, globalns=dict(current_vars))
187
- for attr_name, attr_value in current_vars.items():
188
- if isinstance(attr_value, ArgumentSpec):
189
- attr_value = typing.cast(ArgumentSpec[object], attr_value)
190
- if arguments_spec_type := ArgumentSpecType.from_hint(hints=hints, attr_name=attr_name):
191
- cls.__argspectype__[attr_name] = arguments_spec_type
192
- if attr_value.choices is None and (literals := arguments_spec_type.choices):
193
- attr_value.choices = literals
194
- if attr_value.type is None and (type := arguments_spec_type.type):
195
- attr_value.type = type
196
- if tuple_nargs := arguments_spec_type.tuple_nargs:
197
- attr_value.nargs = tuple_nargs
198
- cls.__argspec__[attr_name] = attr_value
199
- except Exception as e:
200
- warnings.warn(f"Could not fully analyze type hints for {current_cls.__name__}: {e}", stacklevel=2)
201
- for attr_name, attr_value in current_vars.items():
202
- if isinstance(attr_value, ArgumentSpec) and attr_name not in cls.__argspec__:
203
- cls.__argspec__[attr_name] = attr_value
204
-
205
- @classmethod
206
- def iter_specs(cls) -> Iterable[Tuple[str, ArgumentSpec[object]]]:
207
- """Iterates over the registered (attribute_name, ArgumentSpec) pairs."""
208
- yield from cls.__argspec__.items()
209
-
210
- @classmethod
211
- def get_parser(cls) -> argparse.ArgumentParser:
212
- """Creates and configures an ArgumentParser based on the defined ArgumentSpecs."""
213
- arg_parser = argparse.ArgumentParser(
214
- description=cls.__doc__, # Use class docstring as description
215
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
216
- add_help=False, # Add custom help argument later
217
- )
218
- # Add standard help argument
219
- arg_parser.add_argument(
220
- "-h", "--help", action="help", default=argparse.SUPPRESS, help="Show this help message and exit."
221
- )
222
- # Add arguments to the parser based on registered ArgumentSpecs
223
- for key, spec in cls.iter_specs():
224
- kwargs = spec.get_add_argument_kwargs()
225
- # Determine if it's a positional or optional argument
226
- is_positional: bool = not any(name.startswith("-") for name in spec.name_or_flags)
227
- if is_positional:
228
- # For positional args: remove 'required' (implicit), let argparse derive 'dest'
229
- kwargs.pop("required", None)
230
- try:
231
- arg_parser.add_argument(*spec.name_or_flags, **kwargs) # pyright: ignore[reportArgumentType]
232
- except Exception as e:
233
- # Provide informative error message
234
- raise ValueError(
235
- f"Error adding positional argument '{key}' with spec {spec.name_or_flags} and kwargs {kwargs}: {e}"
236
- ) from e
237
- else: # Optional argument
238
- try:
239
- # For optional args: explicitly set 'dest' to the attribute name ('key')
240
- arg_parser.add_argument(*spec.name_or_flags, dest=key, **kwargs) # pyright: ignore[reportArgumentType]
241
- except Exception as e:
242
- # Provide informative error message
243
- raise ValueError(
244
- f"Error adding optional argument '{key}' with spec {spec.name_or_flags} and kwargs {kwargs}: {e}"
245
- ) from e
246
- return arg_parser
247
-
248
- @classmethod
249
- def load(cls, args: Optional[Sequence[str]] = None) -> None:
250
- """
251
- Parses command-line arguments and assigns the values to the corresponding ArgumentSpec instances.
252
- If 'args' is None, uses sys.argv[1:].
253
- """
254
- parser = cls.get_parser()
255
- try:
256
- parsed_args = parser.parse_args(args)
257
- except SystemExit as e:
258
- # Allow SystemExit (e.g., from --help) to propagate
259
- raise e
260
- # Assign parsed values from the namespace
261
- cls.load_from_namespace(parsed_args)
262
-
263
- @classmethod
264
- def load_from_namespace(cls, args: argparse.Namespace) -> None:
265
- """Assigns values from a parsed argparse.Namespace object to the ArgumentSpecs."""
266
- for key, spec in cls.iter_specs():
267
- # Determine the attribute name in the namespace
268
- # Positional args use their name, optionals use the 'dest' (which is 'key')
269
- is_positional = not any(name.startswith("-") for name in spec.name_or_flags)
270
- attr_name = spec.name_or_flags[0] if is_positional else key
271
- # Check if the attribute exists in the namespace
272
- if not hasattr(args, attr_name):
273
- continue
274
-
275
- value: object = getattr(args, attr_name)
276
- if value is argparse.SUPPRESS:
277
- continue
278
-
279
- # Assign the value unless it's the SUPPRESS sentinel
280
- if argument_spec_type := cls.__argspectype__.get(key):
281
- if argument_spec_type.should_return_as_list:
282
- if isinstance(value, list):
283
- value = typing.cast(List[object], value)
284
- elif value is not None:
285
- value = [value]
286
- elif argument_spec_type.should_return_as_tuple:
287
- if isinstance(value, tuple):
288
- value = typing.cast(Tuple[object, ...], value)
289
- elif value is not None:
290
- if isinstance(value, list):
291
- value = tuple(typing.cast(List[object], value))
292
- else:
293
- value = (value,)
294
- spec.value = value
295
-
296
- @classmethod
297
- def get_value(cls, key: str) -> Optional[object]:
298
- """Retrieves the parsed value for a specific argument by its attribute name."""
299
- if key in cls.__argspec__:
300
- return cls.__argspec__[key].value
301
- raise KeyError(f"Argument spec with key '{key}' not found.")
302
-
303
- @classmethod
304
- def get_all_values(cls) -> Dict[str, Optional[object]]:
305
- """Returns a dictionary of all argument attribute names and their parsed values."""
306
- return {key: spec.value for key, spec in cls.iter_specs()}
307
-
308
- def __init__(self) -> None:
309
- self.load()
310
-
311
-
312
- def get_args(t: object) -> Tuple[object, ...]:
313
- """Returns the arguments of a type or a generic type."""
314
- return typing.get_args(t)
315
-
316
-
317
- def get_origin(t: object) -> typing.Optional[object]:
318
- """Returns the origin of a type or a generic type."""
319
- return typing.get_origin(t)
320
-
321
-
322
- # --- Main execution block (Example Usage) ---
323
- if __name__ == "__main__":
324
-
325
- class __Arguments(BaseArguments):
326
- """Example argument parser demonstrating various features."""
327
-
328
- my_str_arg: ArgumentSpec[str] = ArgumentSpec(
329
- ["-s", "--string-arg"], default="Hello", help="A string argument.", metavar="TEXT"
330
- )
331
- my_int_arg: ArgumentSpec[int] = ArgumentSpec(
332
- ["-i", "--integer-arg"], required=True, help="A required integer argument."
333
- )
334
- verbose: ArgumentSpec[bool] = ArgumentSpec(
335
- ["-v", "--verbose"], action="store_true", help="Increase output verbosity."
336
- )
337
- # --- List<str> ---
338
- my_list_arg: ArgumentSpec[List[str]] = ArgumentSpec(
339
- ["--list-values"],
340
- nargs="+",
341
- help="One or more string values.",
342
- default=None,
343
- )
344
- # --- Positional IO ---
345
- input_file: ArgumentSpec[IO[str]] = ArgumentSpec(
346
- ["input_file"],
347
- type=argparse.FileType("r", encoding="utf-8"),
348
- help="Path to the input file (required).",
349
- metavar="INPUT_PATH",
350
- )
351
- output_file: ArgumentSpec[Optional[IO[str]]] = ArgumentSpec(
352
- ["output_file"],
353
- type=argparse.FileType("w", encoding="utf-8"),
354
- nargs="?",
355
- default=None,
356
- help="Optional output file path.",
357
- metavar="OUTPUT_PATH",
358
- )
359
- # --- Simple Literal (choices auto-detected) ---
360
- log_level: ArgumentSpec[Literal["DEBUG", "INFO", "WARNING", "ERROR"]] = ArgumentSpec(
361
- ["--log-level"],
362
- default="INFO",
363
- help="Set the logging level.",
364
- )
365
- # --- Literal + explicit choices (explicit wins) ---
366
- mode: ArgumentSpec[Literal["fast", "slow", "careful"]] = ArgumentSpec(
367
- ["--mode"],
368
- choices=["fast", "slow"], # Explicit choices override Literal args
369
- default="fast",
370
- help="Operation mode.",
371
- )
372
- # --- List[Literal] (choices auto-detected) ---
373
- enabled_features: ArgumentSpec[List[Literal["CACHE", "LOGGING", "RETRY"]]] = ArgumentSpec(
374
- ["--features"],
375
- nargs="*", # 0 or more features
376
- help="Enable specific features.",
377
- default=[],
378
- )
379
- tuple_features: ArgumentSpec[
380
- Tuple[Literal["CACHE", "LOGGING", "RETRY"], Literal["CAwCHE", "LOGGING", "RETRY"]]
381
- ] = ArgumentSpec(
382
- ["--tuple-features"],
383
- help="Enable specific features (tuple).",
384
- )
385
-
386
- # --- SUPPRESS default ---
387
- optional_flag: ArgumentSpec[str] = ArgumentSpec(
388
- ["--opt-flag"],
389
- default=SUPPRESS,
390
- help="An optional flag whose attribute might not be set.",
391
- )
392
-
393
- print("--- Initial State (Before Parsing) ---")
394
- parser_for_debug = __Arguments.get_parser()
395
- for k, s in __Arguments.iter_specs():
396
- print(f"{k}: value={s.value}, type={s.type}, choices={s.choices}") # Check inferred choices
397
-
398
- dummy_input_filename = "temp_input_for_argparse_test.txt"
399
- try:
400
- with open(dummy_input_filename, "w", encoding="utf-8") as f:
401
- f.write("This is a test file.\n")
402
- print(f"\nCreated dummy input file: {dummy_input_filename}")
403
- except Exception as e:
404
- print(f"Warning: Could not create dummy input file '{dummy_input_filename}': {e}")
405
-
406
- # Example command-line arguments (Adjusted order)
407
- test_args = [
408
- dummy_input_filename,
409
- "-i",
410
- "42",
411
- "--log-level",
412
- "WARNING",
413
- "--mode",
414
- "slow",
415
- "--list-values",
416
- "apple",
417
- "banana",
418
- "--features",
419
- "CACHE",
420
- "RETRY", # Test List[Literal]
421
- "--tuple-features",
422
- "CACHE",
423
- "LOGGING", # Test Tuple[Literal]
424
- ]
425
- # test_args = ['--features', 'INVALID'] # Test invalid choice for List[Literal]
426
- # test_args = ['-h']
427
-
428
- try:
429
- print(f"\n--- Loading Arguments (Args: {test_args if test_args else 'from sys.argv'}) ---")
430
- __Arguments.load(test_args)
431
- print("\n--- Final Loaded Arguments ---")
432
- all_values = __Arguments.get_all_values()
433
- for key, value in all_values.items():
434
- value_type = type(value).__name__
435
- if isinstance(value, io.IOBase):
436
- try:
437
- name = getattr(value, "name", "<unknown_name>")
438
- mode = getattr(value, "mode", "?")
439
- value_repr = f"<IO {name} mode='{mode}'>"
440
- except ValueError:
441
- value_repr = "<IO object (closed)>"
442
- else:
443
- value_repr = repr(value)
444
- print(f"{key}: {value_repr} (Type: {value_type})")
445
-
446
- print("\n--- Accessing Specific Values ---")
447
- print(f"Features : {__Arguments.get_value('enabled_features')}") # Check List[Literal] value
448
-
449
- input_f = __Arguments.get_value("input_file")
450
- if isinstance(input_f, io.IOBase):
451
- try:
452
- print(f"\nReading from input file: {input_f.name}") # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
453
- input_f.close()
454
- print(f"Closed input file: {input_f.name}") # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
455
- except Exception as e:
456
- print(f"Error processing input file: {e}")
457
-
458
- except SystemExit as e:
459
- print(f"\nExiting application (SystemExit code: {e.code}).")
460
- except FileNotFoundError as e:
461
- print(f"\nError: Required file not found: {e.filename}")
462
- parser_for_debug.print_usage()
463
- except Exception as e:
464
- print(f"\nAn unexpected error occurred: {e}")
465
- import traceback
466
-
467
- traceback.print_exc()
468
- finally:
469
- import os
470
-
471
- if os.path.exists(dummy_input_filename):
472
- try:
473
- os.remove(dummy_input_filename)
474
- print(f"\nRemoved dummy input file: {dummy_input_filename}")
475
- except Exception as e:
476
- print(f"Warning: Could not remove dummy file '{dummy_input_filename}': {e}")