deepagents-cli 0.0.27__tar.gz → 0.0.28__tar.gz
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.
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/CHANGELOG.md +15 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/PKG-INFO +1 -1
- deepagents_cli-0.0.28/deepagents_cli/_version.py +3 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/app.py +10 -3
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/config.py +19 -3
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/input.py +143 -33
- deepagents_cli-0.0.27/deepagents_cli/image_utils.py → deepagents_cli-0.0.28/deepagents_cli/media_utils.py +184 -9
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/textual_adapter.py +11 -7
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/tool_display.py +13 -2
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/autocomplete.py +90 -25
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/chat_input.py +74 -47
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/model_selector.py +18 -8
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/status.py +54 -23
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/pyproject.toml +1 -1
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_app.py +1 -1
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_autocomplete.py +114 -2
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_chat_input.py +143 -10
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_compact.py +2 -2
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_config.py +2 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_exception_handling.py +16 -16
- deepagents_cli-0.0.28/tests/unit_tests/test_media_utils.py +733 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_model_selector.py +83 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_ui.py +28 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/uv.lock +1 -1
- deepagents_cli-0.0.27/deepagents_cli/_version.py +0 -3
- deepagents_cli-0.0.27/tests/unit_tests/test_image_utils.py +0 -360
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/.gitignore +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/Makefile +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/README.md +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/__main__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/agent.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/app.tcss +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/built_in_skills/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/built_in_skills/skill-creator/SKILL.md +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/built_in_skills/skill-creator/scripts/init_skill.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/built_in_skills/skill-creator/scripts/quick_validate.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/clipboard.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/default_agent_prompt.md +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/file_ops.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/integrations/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/integrations/daytona.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/integrations/langsmith.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/integrations/modal.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/integrations/runloop.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/integrations/sandbox_factory.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/integrations/sandbox_provider.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/local_context.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/main.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/model_config.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/non_interactive.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/project_utils.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/py.typed +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/sessions.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/skills/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/skills/commands.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/skills/load.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/subagents.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/system_prompt.md +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/tools.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/ui.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/update_check.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/_links.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/approval.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/diff.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/history.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/loading.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/message_store.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/messages.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/thread_selector.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/tool_renderers.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/tool_widgets.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/deepagents_cli/widgets/welcome.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/examples/skills/arxiv-search/SKILL.md +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/examples/skills/arxiv-search/arxiv_search.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/examples/skills/langgraph-docs/SKILL.md +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/examples/skills/skill-creator/SKILL.md +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/examples/skills/skill-creator/scripts/init_skill.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/examples/skills/skill-creator/scripts/quick_validate.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/examples/skills/web-research/SKILL.md +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/images/cli.png +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/scripts/check_imports.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/README.md +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/integration_tests/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/integration_tests/benchmarks/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/integration_tests/benchmarks/test_startup_benchmarks.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/integration_tests/conftest.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/integration_tests/test_sandbox_factory.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/integration_tests/test_sandbox_operations.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/conftest.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/skills/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/skills/test_commands.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/skills/test_load.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_agent.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_approval.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_args.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_charset.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_compact_tool.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_end_to_end.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_file_ops.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_history.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_imports.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_input_parsing.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_local_context.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_main.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_main_args.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_message_store.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_messages.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_model_config.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_model_switch.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_non_interactive.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_sessions.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_shell_allow_list.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_subagents.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_textual_adapter.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_thread_selector.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_token_tracker.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_update_check.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_version.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/test_welcome.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/tools/__init__.py +0 -0
- {deepagents_cli-0.0.27 → deepagents_cli-0.0.28}/tests/unit_tests/tools/test_fetch_url.py +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.28](https://github.com/langchain-ai/deepagents/compare/deepagents-cli==0.0.27...deepagents-cli==0.0.28) (2026-03-05)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* Add video support to multimodal inputs ([#1521](https://github.com/langchain-ai/deepagents/issues/1521)) ([f9b49b7](https://github.com/langchain-ai/deepagents/commit/f9b49b7341bd42b5278a03496743e4709689598e))
|
|
8
|
+
* Add NVIDIA api key support and default model ([#1577](https://github.com/langchain-ai/deepagents/issues/1577)) ([9ce2660](https://github.com/langchain-ai/deepagents/commit/9ce2660a67c3497cff18d27131fb7ef49e85b310))
|
|
9
|
+
* Fuzzy search for slash command autocomplete ([#1660](https://github.com/langchain-ai/deepagents/issues/1660)) ([5f6e9c0](https://github.com/langchain-ai/deepagents/commit/5f6e9c014e6a99783b3113184cc12f0179a902f0))
|
|
10
|
+
* Tab autocomplete in model selector ([#1669](https://github.com/langchain-ai/deepagents/issues/1669)) ([28bd0aa](https://github.com/langchain-ai/deepagents/commit/28bd0aaca737b8bb194ecb9f6612989b9aacec02))
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* Backspace at cursor position 0 exits mode even with text ([#1666](https://github.com/langchain-ai/deepagents/issues/1666)) ([dfa4c1f](https://github.com/langchain-ai/deepagents/commit/dfa4c1fedcecf2bb17d8ffef01cf50efe6c80fb0))
|
|
15
|
+
* Skip auto-approve toggle when modal screen is open ([#1668](https://github.com/langchain-ai/deepagents/issues/1668)) ([6597f0b](https://github.com/langchain-ai/deepagents/commit/6597f0b8da3c3bd701a42e228660d459cefe3f64))
|
|
16
|
+
* Truncate model name in status bar on narrow terminals ([#1665](https://github.com/langchain-ai/deepagents/issues/1665)) ([0e24a04](https://github.com/langchain-ai/deepagents/commit/0e24a04aa9e5894735522ce23295bb27fd2b8190))
|
|
17
|
+
|
|
3
18
|
## [0.0.27](https://github.com/langchain-ai/deepagents/compare/deepagents-cli==0.0.26...deepagents-cli==0.0.27) (2026-03-04)
|
|
4
19
|
|
|
5
20
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagents-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.28
|
|
4
4
|
Summary: Terminal interface for Deep Agents - interactive AI agent with file operations, shell access, and sub-agent capabilities.
|
|
5
5
|
Project-URL: Homepage, https://docs.langchain.com/oss/python/deepagents/overview
|
|
6
6
|
Project-URL: Documentation, https://reference.langchain.com/python/deepagents/
|
|
@@ -497,9 +497,9 @@ class DeepAgentsApp(App):
|
|
|
497
497
|
self._message_store = MessageStore()
|
|
498
498
|
# Lazily imported here to avoid pulling image dependencies into
|
|
499
499
|
# argument parsing paths.
|
|
500
|
-
from deepagents_cli.input import
|
|
500
|
+
from deepagents_cli.input import MediaTracker
|
|
501
501
|
|
|
502
|
-
self._image_tracker =
|
|
502
|
+
self._image_tracker = MediaTracker()
|
|
503
503
|
|
|
504
504
|
def compose(self) -> ComposeResult:
|
|
505
505
|
"""Compose the application layout.
|
|
@@ -2484,6 +2484,10 @@ class DeepAgentsApp(App):
|
|
|
2484
2484
|
web search, URL fetch) run without prompting. Updates the status
|
|
2485
2485
|
bar indicator and session state.
|
|
2486
2486
|
"""
|
|
2487
|
+
# shift+tab is reused for navigation inside modal screens (e.g.
|
|
2488
|
+
# ModelSelectorScreen); skip the toggle so it doesn't fire through.
|
|
2489
|
+
if isinstance(self.screen, ModalScreen):
|
|
2490
|
+
return
|
|
2487
2491
|
self._auto_approve = not self._auto_approve
|
|
2488
2492
|
if self._status_bar:
|
|
2489
2493
|
self._status_bar.set_auto_approve(enabled=self._auto_approve)
|
|
@@ -2892,7 +2896,10 @@ class DeepAgentsApp(App):
|
|
|
2892
2896
|
# Post-swap: update UI and save config
|
|
2893
2897
|
display = f"{settings.model_provider}:{settings.model_name}"
|
|
2894
2898
|
if self._status_bar:
|
|
2895
|
-
self._status_bar.set_model(
|
|
2899
|
+
self._status_bar.set_model(
|
|
2900
|
+
provider=settings.model_provider or "",
|
|
2901
|
+
model=settings.model_name or "",
|
|
2902
|
+
)
|
|
2896
2903
|
|
|
2897
2904
|
config_saved = save_recent_model(display)
|
|
2898
2905
|
if config_saved:
|
|
@@ -383,6 +383,7 @@ class Settings:
|
|
|
383
383
|
openai_api_key: OpenAI API key if available.
|
|
384
384
|
anthropic_api_key: Anthropic API key if available.
|
|
385
385
|
google_api_key: Google API key if available.
|
|
386
|
+
nvidia_api_key: NVIDIA API key if available.
|
|
386
387
|
tavily_api_key: Tavily API key if available.
|
|
387
388
|
google_cloud_project: Google Cloud project ID for VertexAI
|
|
388
389
|
authentication.
|
|
@@ -401,6 +402,7 @@ class Settings:
|
|
|
401
402
|
openai_api_key: str | None
|
|
402
403
|
anthropic_api_key: str | None
|
|
403
404
|
google_api_key: str | None
|
|
405
|
+
nvidia_api_key: str | None
|
|
404
406
|
tavily_api_key: str | None
|
|
405
407
|
|
|
406
408
|
# Google Cloud configuration (for VertexAI)
|
|
@@ -435,6 +437,7 @@ class Settings:
|
|
|
435
437
|
openai_key = os.environ.get("OPENAI_API_KEY")
|
|
436
438
|
anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
437
439
|
google_key = os.environ.get("GOOGLE_API_KEY")
|
|
440
|
+
nvidia_key = os.environ.get("NVIDIA_API_KEY")
|
|
438
441
|
tavily_key = os.environ.get("TAVILY_API_KEY")
|
|
439
442
|
google_cloud_project = os.environ.get("GOOGLE_CLOUD_PROJECT")
|
|
440
443
|
|
|
@@ -459,6 +462,7 @@ class Settings:
|
|
|
459
462
|
openai_api_key=openai_key,
|
|
460
463
|
anthropic_api_key=anthropic_key,
|
|
461
464
|
google_api_key=google_key,
|
|
465
|
+
nvidia_api_key=nvidia_key,
|
|
462
466
|
tavily_api_key=tavily_key,
|
|
463
467
|
google_cloud_project=google_cloud_project,
|
|
464
468
|
deepagents_langchain_project=deepagents_langchain_project,
|
|
@@ -482,6 +486,11 @@ class Settings:
|
|
|
482
486
|
"""Check if Google API key is configured."""
|
|
483
487
|
return self.google_api_key is not None
|
|
484
488
|
|
|
489
|
+
@property
|
|
490
|
+
def has_nvidia(self) -> bool:
|
|
491
|
+
"""Check if NVIDIA API key is configured."""
|
|
492
|
+
return self.nvidia_api_key is not None
|
|
493
|
+
|
|
485
494
|
@property
|
|
486
495
|
def has_vertex_ai(self) -> bool:
|
|
487
496
|
"""Check if VertexAI is available (Google Cloud project set, no API key).
|
|
@@ -1084,8 +1093,9 @@ def detect_provider(model_name: str) -> str | None:
|
|
|
1084
1093
|
model_name: Model name to detect provider from.
|
|
1085
1094
|
|
|
1086
1095
|
Returns:
|
|
1087
|
-
Provider name (openai, anthropic, google_genai, google_vertexai
|
|
1088
|
-
`None` if the provider cannot be determined from the
|
|
1096
|
+
Provider name (openai, anthropic, google_genai, google_vertexai,
|
|
1097
|
+
nvidia) or `None` if the provider cannot be determined from the
|
|
1098
|
+
name alone.
|
|
1089
1099
|
"""
|
|
1090
1100
|
model_lower = model_name.lower()
|
|
1091
1101
|
|
|
@@ -1102,6 +1112,9 @@ def detect_provider(model_name: str) -> str | None:
|
|
|
1102
1112
|
return "google_vertexai"
|
|
1103
1113
|
return "google_genai"
|
|
1104
1114
|
|
|
1115
|
+
if model_lower.startswith(("nemotron", "nvidia/")):
|
|
1116
|
+
return "nvidia"
|
|
1117
|
+
|
|
1105
1118
|
return None
|
|
1106
1119
|
|
|
1107
1120
|
|
|
@@ -1135,11 +1148,13 @@ def _get_default_model_spec() -> str:
|
|
|
1135
1148
|
return "google_genai:gemini-3.1-pro-preview"
|
|
1136
1149
|
if settings.has_vertex_ai:
|
|
1137
1150
|
return "google_vertexai:gemini-3.1-pro-preview"
|
|
1151
|
+
if settings.has_nvidia:
|
|
1152
|
+
return "nvidia:nvidia/nemotron-3-nano-30b-a3b"
|
|
1138
1153
|
|
|
1139
1154
|
msg = (
|
|
1140
1155
|
"No credentials configured. Please set one of: "
|
|
1141
1156
|
"ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, "
|
|
1142
|
-
"or
|
|
1157
|
+
"GOOGLE_CLOUD_PROJECT, or NVIDIA_API_KEY"
|
|
1143
1158
|
)
|
|
1144
1159
|
raise ModelConfigError(msg)
|
|
1145
1160
|
|
|
@@ -1309,6 +1324,7 @@ def _create_model_via_init(
|
|
|
1309
1324
|
"openai": "langchain-openai",
|
|
1310
1325
|
"google_genai": "langchain-google-genai",
|
|
1311
1326
|
"google_vertexai": "langchain-google-vertexai",
|
|
1327
|
+
"nvidia": "langchain-nvidia-ai-endpoints",
|
|
1312
1328
|
}
|
|
1313
1329
|
package = package_map.get(provider, f"langchain-{provider}")
|
|
1314
1330
|
msg = (
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
"""Input handling utilities including image tracking and file mention parsing."""
|
|
1
|
+
"""Input handling utilities including image/video tracking and file mention parsing."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
5
|
import shlex
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
8
9
|
from urllib.parse import unquote, urlparse
|
|
9
10
|
|
|
10
11
|
from deepagents_cli.config import console
|
|
11
|
-
from deepagents_cli.
|
|
12
|
+
from deepagents_cli.media_utils import ImageData, VideoData
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
@@ -52,6 +53,9 @@ in `UserMessage.compose()` additionally checks `start == 0` before styling
|
|
|
52
53
|
slash commands, so a `/` mid-string is not highlighted.
|
|
53
54
|
"""
|
|
54
55
|
|
|
56
|
+
MediaKind = Literal["image", "video"]
|
|
57
|
+
"""Accepted values for the `kind` parameter in `MediaTracker` methods."""
|
|
58
|
+
|
|
55
59
|
IMAGE_PLACEHOLDER_PATTERN = re.compile(r"\[image (?P<id>\d+)\]")
|
|
56
60
|
"""Pattern for image placeholders with a named `id` capture group.
|
|
57
61
|
|
|
@@ -59,6 +63,13 @@ Used to extract numeric IDs from placeholder tokens so the tracker can prune
|
|
|
59
63
|
stale entries and compute the next available ID.
|
|
60
64
|
"""
|
|
61
65
|
|
|
66
|
+
VIDEO_PLACEHOLDER_PATTERN = re.compile(r"\[video (?P<id>\d+)\]")
|
|
67
|
+
"""Pattern for video placeholders with a named `id` capture group.
|
|
68
|
+
|
|
69
|
+
Used to extract numeric IDs from placeholder tokens so the tracker can prune
|
|
70
|
+
stale entries and compute the next available ID.
|
|
71
|
+
"""
|
|
72
|
+
|
|
62
73
|
_UNICODE_SPACE_EQUIVALENTS = str.maketrans(
|
|
63
74
|
{
|
|
64
75
|
"\u00a0": " ", # NO-BREAK SPACE
|
|
@@ -91,32 +102,76 @@ class ParsedPastedPathPayload:
|
|
|
91
102
|
token_end: int | None = None
|
|
92
103
|
|
|
93
104
|
|
|
94
|
-
class
|
|
95
|
-
"""Track pasted images in the current conversation."""
|
|
105
|
+
class MediaTracker:
|
|
106
|
+
"""Track pasted images and videos in the current conversation."""
|
|
96
107
|
|
|
97
108
|
def __init__(self) -> None:
|
|
98
|
-
"""Initialize an empty
|
|
109
|
+
"""Initialize an empty media tracker.
|
|
99
110
|
|
|
100
|
-
Sets up
|
|
101
|
-
to 1 for generating unique placeholder identifiers.
|
|
111
|
+
Sets up empty lists to store images and videos, and initializes the
|
|
112
|
+
ID counters to 1 for generating unique placeholder identifiers.
|
|
102
113
|
"""
|
|
103
114
|
self.images: list[ImageData] = []
|
|
104
|
-
self.
|
|
115
|
+
self.videos: list[VideoData] = []
|
|
116
|
+
self.next_image_id: int = 1
|
|
117
|
+
self.next_video_id: int = 1
|
|
118
|
+
|
|
119
|
+
def add_media(self, data: ImageData | VideoData, kind: MediaKind) -> str:
|
|
120
|
+
"""Add a media item and return its placeholder text.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
data: The image or video data to track.
|
|
124
|
+
kind: Media type key.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Placeholder string like "[image 1]" or "[video 1]".
|
|
128
|
+
"""
|
|
129
|
+
if kind == "image":
|
|
130
|
+
placeholder = f"[image {self.next_image_id}]"
|
|
131
|
+
data.placeholder = placeholder
|
|
132
|
+
self.images.append(data) # type: ignore[arg-type]
|
|
133
|
+
self.next_image_id += 1
|
|
134
|
+
else:
|
|
135
|
+
placeholder = f"[video {self.next_video_id}]"
|
|
136
|
+
data.placeholder = placeholder
|
|
137
|
+
self.videos.append(data) # type: ignore[arg-type]
|
|
138
|
+
self.next_video_id += 1
|
|
139
|
+
return placeholder
|
|
105
140
|
|
|
106
141
|
def add_image(self, image_data: ImageData) -> str:
|
|
107
142
|
"""Add an image and return its placeholder text.
|
|
108
143
|
|
|
109
144
|
Args:
|
|
110
|
-
image_data: The image data to track
|
|
145
|
+
image_data: The image data to track.
|
|
111
146
|
|
|
112
147
|
Returns:
|
|
113
|
-
Placeholder string like "[image 1]"
|
|
148
|
+
Placeholder string like "[image 1]".
|
|
114
149
|
"""
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
150
|
+
return self.add_media(image_data, "image")
|
|
151
|
+
|
|
152
|
+
def add_video(self, video_data: VideoData) -> str:
|
|
153
|
+
"""Add a video and return its placeholder text.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
video_data: The video data to track.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Placeholder string like "[video 1]".
|
|
160
|
+
"""
|
|
161
|
+
return self.add_media(video_data, "video")
|
|
162
|
+
|
|
163
|
+
def get_media(self, kind: MediaKind) -> list[ImageData] | list[VideoData]:
|
|
164
|
+
"""Get all tracked media of a given type.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
kind: Media type key.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Copy of the list of tracked media items.
|
|
171
|
+
"""
|
|
172
|
+
if kind == "image":
|
|
173
|
+
return list(self.images)
|
|
174
|
+
return list(self.videos)
|
|
120
175
|
|
|
121
176
|
def get_images(self) -> list[ImageData]:
|
|
122
177
|
"""Get all tracked images.
|
|
@@ -124,39 +179,94 @@ class ImageTracker:
|
|
|
124
179
|
Returns:
|
|
125
180
|
Copy of the list of tracked images.
|
|
126
181
|
"""
|
|
127
|
-
return self.images
|
|
182
|
+
return list(self.images)
|
|
183
|
+
|
|
184
|
+
def get_videos(self) -> list[VideoData]:
|
|
185
|
+
"""Get all tracked videos.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Copy of the list of tracked videos.
|
|
189
|
+
"""
|
|
190
|
+
return list(self.videos)
|
|
128
191
|
|
|
129
192
|
def clear(self) -> None:
|
|
130
|
-
"""Clear all tracked
|
|
193
|
+
"""Clear all tracked media and reset counters."""
|
|
131
194
|
self.images.clear()
|
|
132
|
-
self.
|
|
195
|
+
self.videos.clear()
|
|
196
|
+
self.next_image_id = 1
|
|
197
|
+
self.next_video_id = 1
|
|
133
198
|
|
|
134
199
|
def sync_to_text(self, text: str) -> None:
|
|
135
|
-
"""Retain only
|
|
200
|
+
"""Retain only media still referenced by placeholders in current text.
|
|
136
201
|
|
|
137
202
|
Args:
|
|
138
203
|
text: Current input text shown to the user.
|
|
139
204
|
"""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if not placeholders:
|
|
205
|
+
img_found = self._sync_kind_images(text)
|
|
206
|
+
vid_found = self._sync_kind_videos(text)
|
|
207
|
+
if not img_found and not vid_found:
|
|
144
208
|
self.clear()
|
|
145
|
-
return
|
|
146
209
|
|
|
210
|
+
def _sync_kind_images(self, text: str) -> bool:
|
|
211
|
+
"""Sync image list to surviving placeholders in text.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
text: Current input text.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Whether any image placeholders were found.
|
|
218
|
+
"""
|
|
219
|
+
placeholders = {m.group(0) for m in IMAGE_PLACEHOLDER_PATTERN.finditer(text)}
|
|
147
220
|
self.images = [img for img in self.images if img.placeholder in placeholders]
|
|
148
221
|
if not self.images:
|
|
149
|
-
self.
|
|
150
|
-
|
|
222
|
+
self.next_image_id = 1
|
|
223
|
+
else:
|
|
224
|
+
self.next_image_id = self._max_placeholder_id(
|
|
225
|
+
self.images, IMAGE_PLACEHOLDER_PATTERN, len(self.images)
|
|
226
|
+
)
|
|
227
|
+
return bool(placeholders)
|
|
151
228
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
229
|
+
def _sync_kind_videos(self, text: str) -> bool:
|
|
230
|
+
"""Sync video list to surviving placeholders in text.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
text: Current input text.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Whether any video placeholders were found.
|
|
237
|
+
"""
|
|
238
|
+
placeholders = {m.group(0) for m in VIDEO_PLACEHOLDER_PATTERN.finditer(text)}
|
|
239
|
+
self.videos = [vid for vid in self.videos if vid.placeholder in placeholders]
|
|
240
|
+
if not self.videos:
|
|
241
|
+
self.next_video_id = 1
|
|
242
|
+
else:
|
|
243
|
+
self.next_video_id = self._max_placeholder_id(
|
|
244
|
+
self.videos, VIDEO_PLACEHOLDER_PATTERN, len(self.videos)
|
|
245
|
+
)
|
|
246
|
+
return bool(placeholders)
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def _max_placeholder_id(
|
|
250
|
+
items: list[ImageData] | list[VideoData],
|
|
251
|
+
pattern: re.Pattern[str],
|
|
252
|
+
fallback_count: int,
|
|
253
|
+
) -> int:
|
|
254
|
+
"""Compute next ID from the highest surviving placeholder.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
items: Surviving media items.
|
|
258
|
+
pattern: Placeholder regex with an `id` group.
|
|
259
|
+
fallback_count: Fallback when no IDs can be parsed.
|
|
158
260
|
|
|
159
|
-
|
|
261
|
+
Returns:
|
|
262
|
+
Next ID value (max_id + 1).
|
|
263
|
+
"""
|
|
264
|
+
max_id = 0
|
|
265
|
+
for item in items:
|
|
266
|
+
match = pattern.fullmatch(item.placeholder)
|
|
267
|
+
if match is not None:
|
|
268
|
+
max_id = max(max_id, int(match.group("id")))
|
|
269
|
+
return max_id + 1 if max_id else fallback_count + 1
|
|
160
270
|
|
|
161
271
|
|
|
162
272
|
def parse_file_mentions(text: str) -> tuple[str, list[Path]]:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Utilities for handling image
|
|
1
|
+
"""Utilities for handling image and video media from clipboard and files."""
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import io
|
|
@@ -12,9 +12,43 @@ import subprocess # noqa: S404
|
|
|
12
12
|
import sys
|
|
13
13
|
import tempfile
|
|
14
14
|
from dataclasses import dataclass
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from langchain_core.messages.content import VideoContentBlock
|
|
15
19
|
|
|
16
20
|
logger = logging.getLogger(__name__)
|
|
17
21
|
|
|
22
|
+
IMAGE_EXTENSIONS: frozenset[str] = frozenset(
|
|
23
|
+
{
|
|
24
|
+
".png",
|
|
25
|
+
".jpg",
|
|
26
|
+
".jpeg",
|
|
27
|
+
".gif",
|
|
28
|
+
".bmp",
|
|
29
|
+
".tiff",
|
|
30
|
+
".tif",
|
|
31
|
+
".webp",
|
|
32
|
+
".ico",
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
"""Common image file extensions supported by PIL."""
|
|
36
|
+
|
|
37
|
+
VIDEO_EXTENSIONS: frozenset[str] = frozenset(
|
|
38
|
+
{
|
|
39
|
+
".mp4",
|
|
40
|
+
".mov",
|
|
41
|
+
".avi",
|
|
42
|
+
".webm",
|
|
43
|
+
".m4v",
|
|
44
|
+
".wmv",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
"""Video file extensions with validated magic-byte support."""
|
|
48
|
+
|
|
49
|
+
MAX_MEDIA_BYTES: int = 20 * 1024 * 1024
|
|
50
|
+
"""Maximum media file size (20 MB). Keeps base64 payload under ~27 MB."""
|
|
51
|
+
|
|
18
52
|
|
|
19
53
|
def _get_executable(name: str) -> str | None:
|
|
20
54
|
"""Get full path to an executable using shutil.which().
|
|
@@ -48,6 +82,28 @@ class ImageData:
|
|
|
48
82
|
}
|
|
49
83
|
|
|
50
84
|
|
|
85
|
+
@dataclass
|
|
86
|
+
class VideoData:
|
|
87
|
+
"""Represents a pasted video with its base64 encoding."""
|
|
88
|
+
|
|
89
|
+
base64_data: str
|
|
90
|
+
format: str # "mp4", "quicktime", etc.
|
|
91
|
+
placeholder: str # Display text like "[video 1]"
|
|
92
|
+
|
|
93
|
+
def to_message_content(self) -> "VideoContentBlock":
|
|
94
|
+
"""Convert to LangChain `VideoContentBlock` format.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
`VideoContentBlock` with base64 data and mime_type.
|
|
98
|
+
"""
|
|
99
|
+
from langchain_core.messages.content import create_video_block
|
|
100
|
+
|
|
101
|
+
return create_video_block(
|
|
102
|
+
base64=self.base64_data,
|
|
103
|
+
mime_type=f"video/{self.format}",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
51
107
|
def get_clipboard_image() -> ImageData | None:
|
|
52
108
|
"""Attempt to read an image from the system clipboard.
|
|
53
109
|
|
|
@@ -79,6 +135,19 @@ def get_image_from_path(path: pathlib.Path) -> ImageData | None:
|
|
|
79
135
|
from PIL import Image, UnidentifiedImageError
|
|
80
136
|
|
|
81
137
|
try:
|
|
138
|
+
file_size = path.stat().st_size
|
|
139
|
+
if file_size == 0:
|
|
140
|
+
logger.debug("Image file is empty: %s", path)
|
|
141
|
+
return None
|
|
142
|
+
if file_size > MAX_MEDIA_BYTES:
|
|
143
|
+
logger.warning(
|
|
144
|
+
"Image file %s is too large (%d MB, max %d MB)",
|
|
145
|
+
path,
|
|
146
|
+
file_size // (1024 * 1024),
|
|
147
|
+
MAX_MEDIA_BYTES // (1024 * 1024),
|
|
148
|
+
)
|
|
149
|
+
return None
|
|
150
|
+
|
|
82
151
|
image_bytes = path.read_bytes()
|
|
83
152
|
if not image_bytes:
|
|
84
153
|
return None
|
|
@@ -95,7 +164,7 @@ def get_image_from_path(path: pathlib.Path) -> ImageData | None:
|
|
|
95
164
|
image_format = "png"
|
|
96
165
|
|
|
97
166
|
return ImageData(
|
|
98
|
-
base64_data=
|
|
167
|
+
base64_data=encode_to_base64(image_bytes),
|
|
99
168
|
format=image_format,
|
|
100
169
|
placeholder="[image]",
|
|
101
170
|
)
|
|
@@ -104,6 +173,105 @@ def get_image_from_path(path: pathlib.Path) -> ImageData | None:
|
|
|
104
173
|
return None
|
|
105
174
|
|
|
106
175
|
|
|
176
|
+
def _detect_video_format(data: bytes) -> str | None:
|
|
177
|
+
"""Detect video MIME subtype from magic bytes.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
data: Raw file bytes (at least 12 bytes for reliable detection).
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
MIME subtype (e.g. "mp4", "webm") or `None` if unrecognized.
|
|
184
|
+
"""
|
|
185
|
+
min_avi_len = 12
|
|
186
|
+
if data[4:8] == b"ftyp":
|
|
187
|
+
# ftyp box: major brand at bytes 8-12 distinguishes MOV vs MP4
|
|
188
|
+
brand = data[8:12]
|
|
189
|
+
if brand == b"qt ":
|
|
190
|
+
return "quicktime"
|
|
191
|
+
return "mp4"
|
|
192
|
+
if data[:4] == b"RIFF" and len(data) >= min_avi_len and data[8:12] == b"AVI ":
|
|
193
|
+
return "avi"
|
|
194
|
+
if data[:4] == b"\x30\x26\xb2\x75": # ASF/WMV
|
|
195
|
+
return "x-ms-wmv"
|
|
196
|
+
if data[:4] == b"\x1a\x45\xdf\xa3": # WebM/Matroska (EBML header)
|
|
197
|
+
return "webm"
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_video_from_path(path: pathlib.Path) -> VideoData | None:
|
|
202
|
+
"""Read and encode a video file from disk.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
path: Path to the video file.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
`VideoData` when the file is a valid video, otherwise `None`.
|
|
209
|
+
"""
|
|
210
|
+
suffix = path.suffix.lower()
|
|
211
|
+
if suffix not in VIDEO_EXTENSIONS:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
file_size = path.stat().st_size
|
|
216
|
+
if file_size == 0:
|
|
217
|
+
logger.debug("Video file is empty: %s", path)
|
|
218
|
+
return None
|
|
219
|
+
if file_size > MAX_MEDIA_BYTES:
|
|
220
|
+
logger.warning(
|
|
221
|
+
"Video file %s is too large (%d MB, max %d MB)",
|
|
222
|
+
path,
|
|
223
|
+
file_size // (1024 * 1024),
|
|
224
|
+
MAX_MEDIA_BYTES // (1024 * 1024),
|
|
225
|
+
)
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
video_bytes = path.read_bytes()
|
|
229
|
+
|
|
230
|
+
# Validate it's a real video file by checking magic bytes
|
|
231
|
+
# MP4 starts with ftyp, MOV also uses ftyp, AVI starts with RIFF
|
|
232
|
+
min_video_len = 8
|
|
233
|
+
if len(video_bytes) < min_video_len:
|
|
234
|
+
logger.debug("Video file too small (%d bytes): %s", len(video_bytes), path)
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
# Detect format from magic bytes (not extension) so renamed files
|
|
238
|
+
# get the correct MIME type.
|
|
239
|
+
detected_format = _detect_video_format(video_bytes)
|
|
240
|
+
if detected_format is None:
|
|
241
|
+
logger.warning(
|
|
242
|
+
"Video file %s has unrecognized signature for extension '%s'; "
|
|
243
|
+
"skipping. If this is a valid video, the format may not be "
|
|
244
|
+
"supported yet.",
|
|
245
|
+
path,
|
|
246
|
+
suffix,
|
|
247
|
+
)
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
return VideoData(
|
|
251
|
+
base64_data=encode_to_base64(video_bytes),
|
|
252
|
+
format=detected_format,
|
|
253
|
+
placeholder="[video]",
|
|
254
|
+
)
|
|
255
|
+
except OSError as e:
|
|
256
|
+
logger.warning("Failed to load video from %s: %s", path, e, exc_info=True)
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def get_media_from_path(path: pathlib.Path) -> ImageData | VideoData | None:
|
|
261
|
+
"""Try to load a file as an image first, then as a video.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
path: Path to the media file.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
`ImageData` or `VideoData` if the file is valid media, otherwise `None`.
|
|
268
|
+
"""
|
|
269
|
+
result: ImageData | VideoData | None = get_image_from_path(path)
|
|
270
|
+
if result is not None:
|
|
271
|
+
return result
|
|
272
|
+
return get_video_from_path(path)
|
|
273
|
+
|
|
274
|
+
|
|
107
275
|
def _get_macos_clipboard_image() -> ImageData | None:
|
|
108
276
|
"""Get clipboard image on macOS using pngpaste or osascript.
|
|
109
277
|
|
|
@@ -269,27 +437,30 @@ def _get_clipboard_via_osascript() -> ImageData | None:
|
|
|
269
437
|
logger.debug("Failed to clean up temp file %s: %s", temp_path, e)
|
|
270
438
|
|
|
271
439
|
|
|
272
|
-
def
|
|
273
|
-
"""Encode
|
|
440
|
+
def encode_to_base64(data: bytes) -> str:
|
|
441
|
+
"""Encode raw bytes to a base64 string.
|
|
274
442
|
|
|
275
443
|
Args:
|
|
276
|
-
|
|
444
|
+
data: Raw bytes to encode.
|
|
277
445
|
|
|
278
446
|
Returns:
|
|
279
447
|
Base64-encoded string.
|
|
280
448
|
"""
|
|
281
|
-
return base64.b64encode(
|
|
449
|
+
return base64.b64encode(data).decode("utf-8")
|
|
282
450
|
|
|
283
451
|
|
|
284
|
-
def create_multimodal_content(
|
|
285
|
-
|
|
452
|
+
def create_multimodal_content(
|
|
453
|
+
text: str, images: list[ImageData], videos: list[VideoData] | None = None
|
|
454
|
+
) -> list[dict]:
|
|
455
|
+
"""Create multimodal message content with text, images, and videos.
|
|
286
456
|
|
|
287
457
|
Args:
|
|
288
458
|
text: Text content of the message
|
|
289
459
|
images: List of ImageData objects
|
|
460
|
+
videos: Optional list of VideoData objects
|
|
290
461
|
|
|
291
462
|
Returns:
|
|
292
|
-
List of content blocks in LangChain format.
|
|
463
|
+
List of content blocks in LangChain message format.
|
|
293
464
|
"""
|
|
294
465
|
content_blocks = []
|
|
295
466
|
|
|
@@ -300,4 +471,8 @@ def create_multimodal_content(text: str, images: list[ImageData]) -> list[dict]:
|
|
|
300
471
|
# Add image blocks
|
|
301
472
|
content_blocks.extend(image.to_message_content() for image in images)
|
|
302
473
|
|
|
474
|
+
# Add video blocks
|
|
475
|
+
if videos:
|
|
476
|
+
content_blocks.extend(video.to_message_content() for video in videos)
|
|
477
|
+
|
|
303
478
|
return content_blocks
|