EvoScientist 0.0.1.dev6__tar.gz → 0.0.1.dev7__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.
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/_app.py +1 -1
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/commands.py +14 -14
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/interactive.py +13 -4
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/config.py +16 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/llm/__init__.py +2 -0
- evoscientist-0.0.1.dev7/EvoScientist/llm/models.py +193 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/memory.py +2 -4
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/onboard.py +271 -47
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/sessions.py +17 -4
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/PKG-INFO +1 -1
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/SOURCES.txt +1 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/PKG-INFO +1 -1
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/pyproject.toml +1 -1
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_llm.py +37 -46
- evoscientist-0.0.1.dev7/tests/test_memory_merge.py +73 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_onboard.py +44 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_sessions.py +53 -0
- evoscientist-0.0.1.dev6/EvoScientist/llm/models.py +0 -115
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/EvoScientist.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/__init__.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/__main__.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/backends.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/__init__.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/base.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/__init__.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/channel_rpc.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/probe.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/rpc_client.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/serve.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/targets.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/__init__.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/agent.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/channel.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/mcp_ui.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/skills_cmd.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/mcp/__init__.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/mcp/client.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/middleware.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/paths.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/prompts.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/find-skills/SKILL.md +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/SKILL.md +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/references/output-patterns.md +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/references/workflows.md +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/scripts/init_skill.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/scripts/package_skill.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/scripts/quick_validate.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/__init__.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/display.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/emitter.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/events.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/formatter.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/state.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/tracker.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/utils.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/subagent.yaml +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/__init__.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/search.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/skill_manager.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/skills_manager.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/think.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/utils.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/dependency_links.txt +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/entry_points.txt +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/requires.txt +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/top_level.txt +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/LICENSE +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/README.md +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/setup.cfg +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_backends.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_cli_run_name.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_config.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_event_loop.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_imports.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_mcp_client.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_prompts.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_skills_manager.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_emitter.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_events.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_state.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_tracker.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_utils.py +0 -0
- {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_tools.py +0 -0
|
@@ -36,7 +36,7 @@ def onboard(
|
|
|
36
36
|
help="Skip API key validation during setup"
|
|
37
37
|
),
|
|
38
38
|
):
|
|
39
|
-
"""Interactive setup wizard for EvoScientist
|
|
39
|
+
"""Interactive setup wizard for EvoScientist
|
|
40
40
|
|
|
41
41
|
Guides you through configuring API keys, model selection,
|
|
42
42
|
workspace settings, and agent parameters.
|
|
@@ -51,14 +51,14 @@ def onboard(
|
|
|
51
51
|
|
|
52
52
|
@config_app.callback(invoke_without_command=True)
|
|
53
53
|
def config_callback(ctx: typer.Context):
|
|
54
|
-
"""Configuration management commands
|
|
54
|
+
"""Configuration management commands"""
|
|
55
55
|
if ctx.invoked_subcommand is None:
|
|
56
56
|
config_list()
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
@config_app.command("list")
|
|
60
60
|
def config_list():
|
|
61
|
-
"""List all configuration values
|
|
61
|
+
"""List all configuration values"""
|
|
62
62
|
from ..config import list_config, get_config_path
|
|
63
63
|
|
|
64
64
|
config_data = list_config()
|
|
@@ -84,7 +84,7 @@ def config_list():
|
|
|
84
84
|
|
|
85
85
|
@config_app.command("get")
|
|
86
86
|
def config_get(key: str = typer.Argument(..., help="Configuration key to get")):
|
|
87
|
-
"""Get a single configuration value
|
|
87
|
+
"""Get a single configuration value"""
|
|
88
88
|
from ..config import get_config_value
|
|
89
89
|
|
|
90
90
|
value = get_config_value(key)
|
|
@@ -108,7 +108,7 @@ def config_set(
|
|
|
108
108
|
key: str = typer.Argument(..., help="Configuration key to set"),
|
|
109
109
|
value: str = typer.Argument(..., help="New value"),
|
|
110
110
|
):
|
|
111
|
-
"""Set a single configuration value
|
|
111
|
+
"""Set a single configuration value"""
|
|
112
112
|
from ..config import set_config_value
|
|
113
113
|
|
|
114
114
|
if set_config_value(key, value):
|
|
@@ -122,7 +122,7 @@ def config_set(
|
|
|
122
122
|
def config_reset(
|
|
123
123
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
124
124
|
):
|
|
125
|
-
"""Reset configuration to defaults
|
|
125
|
+
"""Reset configuration to defaults"""
|
|
126
126
|
from ..config import reset_config, get_config_path
|
|
127
127
|
|
|
128
128
|
config_path = get_config_path()
|
|
@@ -143,7 +143,7 @@ def config_reset(
|
|
|
143
143
|
|
|
144
144
|
@config_app.command("path")
|
|
145
145
|
def config_path():
|
|
146
|
-
"""Show the configuration file path
|
|
146
|
+
"""Show the configuration file path"""
|
|
147
147
|
from ..config import get_config_path
|
|
148
148
|
|
|
149
149
|
path = get_config_path()
|
|
@@ -158,14 +158,14 @@ def config_path():
|
|
|
158
158
|
|
|
159
159
|
@mcp_app.callback(invoke_without_command=True)
|
|
160
160
|
def mcp_callback(ctx: typer.Context):
|
|
161
|
-
"""MCP server management commands
|
|
161
|
+
"""MCP server management commands"""
|
|
162
162
|
if ctx.invoked_subcommand is None:
|
|
163
163
|
mcp_list()
|
|
164
164
|
|
|
165
165
|
|
|
166
166
|
@mcp_app.command("list")
|
|
167
167
|
def mcp_list():
|
|
168
|
-
"""List configured MCP servers
|
|
168
|
+
"""List configured MCP servers"""
|
|
169
169
|
_mcp_list_servers()
|
|
170
170
|
|
|
171
171
|
|
|
@@ -173,7 +173,7 @@ def mcp_list():
|
|
|
173
173
|
def mcp_config(
|
|
174
174
|
name: Optional[str] = typer.Argument(None, help="Server name (omit to show all)"),
|
|
175
175
|
):
|
|
176
|
-
"""Show detailed configuration for MCP servers
|
|
176
|
+
"""Show detailed configuration for MCP servers
|
|
177
177
|
|
|
178
178
|
\b
|
|
179
179
|
Examples:
|
|
@@ -200,7 +200,7 @@ def mcp_add(
|
|
|
200
200
|
env: Optional[list[str]] = typer.Option(None, "--env", help="Env var as KEY=VALUE for stdio (repeatable)"),
|
|
201
201
|
env_ref: Optional[list[str]] = typer.Option(None, "--env-ref", help="Env var name as ${NAME} runtime ref (repeatable)"),
|
|
202
202
|
):
|
|
203
|
-
"""Add an MCP server to user config
|
|
203
|
+
"""Add an MCP server to user config
|
|
204
204
|
|
|
205
205
|
\b
|
|
206
206
|
Transport is auto-detected: URLs default to http, commands default to stdio.
|
|
@@ -249,7 +249,7 @@ def mcp_edit(
|
|
|
249
249
|
header: Optional[list[str]] = typer.Option(None, "--header", "-H", help="HTTP header as Key:Value (repeatable)"),
|
|
250
250
|
env: Optional[list[str]] = typer.Option(None, "--env", help="Env var as KEY=VALUE for stdio (repeatable)"),
|
|
251
251
|
):
|
|
252
|
-
"""Edit an existing MCP server in user config
|
|
252
|
+
"""Edit an existing MCP server in user config
|
|
253
253
|
|
|
254
254
|
\b
|
|
255
255
|
Examples:
|
|
@@ -278,7 +278,7 @@ def mcp_edit(
|
|
|
278
278
|
def mcp_remove(
|
|
279
279
|
name: str = typer.Argument(..., help="Server name to remove"),
|
|
280
280
|
):
|
|
281
|
-
"""Remove an MCP server from user config
|
|
281
|
+
"""Remove an MCP server from user config"""
|
|
282
282
|
if not _mcp_remove_server(name, show_reload_hint=False):
|
|
283
283
|
raise typer.Exit(1)
|
|
284
284
|
|
|
@@ -308,7 +308,7 @@ def _main_callback(
|
|
|
308
308
|
use_cwd: bool = typer.Option(False, "--use-cwd", help="Use current working directory as workspace"),
|
|
309
309
|
no_thinking: bool = typer.Option(False, "--no-thinking", help="Disable thinking display"),
|
|
310
310
|
):
|
|
311
|
-
"""EvoScientist Agent - AI-powered research & code execution CLI
|
|
311
|
+
"""EvoScientist Agent - AI-powered research & code execution CLI"""
|
|
312
312
|
# If a subcommand was invoked, don't run the default behavior
|
|
313
313
|
if ctx.invoked_subcommand is not None:
|
|
314
314
|
return
|
|
@@ -349,7 +349,7 @@ def cmd_interactive(
|
|
|
349
349
|
async def _cmd_threads():
|
|
350
350
|
"""Handle /threads command — show recent sessions."""
|
|
351
351
|
threads = await list_threads(
|
|
352
|
-
limit=
|
|
352
|
+
limit=0, include_message_count=True, include_preview=True,
|
|
353
353
|
)
|
|
354
354
|
if not threads:
|
|
355
355
|
console.print("[yellow]No saved sessions.[/yellow]")
|
|
@@ -423,7 +423,7 @@ def cmd_interactive(
|
|
|
423
423
|
if not arg:
|
|
424
424
|
# Show interactive session picker with conversation previews
|
|
425
425
|
threads = await list_threads(
|
|
426
|
-
limit=
|
|
426
|
+
limit=0, include_message_count=True, include_preview=True,
|
|
427
427
|
)
|
|
428
428
|
if not threads:
|
|
429
429
|
console.print("[yellow]No sessions to resume.[/yellow]")
|
|
@@ -453,11 +453,20 @@ def cmd_interactive(
|
|
|
453
453
|
label = f"{_pad_to_width(left_text, col_width)}({tid} {when})"
|
|
454
454
|
choices.append(questionary.Choice(title=label, value=tid))
|
|
455
455
|
|
|
456
|
-
|
|
456
|
+
from prompt_toolkit.layout.dimension import Dimension
|
|
457
|
+
from questionary.prompts.common import InquirerControl
|
|
458
|
+
|
|
459
|
+
prompt = questionary.select(
|
|
457
460
|
"Select session to resume:",
|
|
458
461
|
choices=choices,
|
|
459
462
|
style=_PICKER_STYLE,
|
|
460
|
-
)
|
|
463
|
+
)
|
|
464
|
+
# Limit visible list to 10 rows with scrolling
|
|
465
|
+
for window in prompt.application.layout.find_all_windows():
|
|
466
|
+
if isinstance(window.content, InquirerControl):
|
|
467
|
+
window.height = Dimension(max=10)
|
|
468
|
+
break
|
|
469
|
+
selected = prompt.ask()
|
|
461
470
|
|
|
462
471
|
if selected is None:
|
|
463
472
|
return
|
|
@@ -63,6 +63,10 @@ class EvoScientistConfig:
|
|
|
63
63
|
openai_api_key: str = ""
|
|
64
64
|
nvidia_api_key: str = ""
|
|
65
65
|
google_api_key: str = ""
|
|
66
|
+
siliconflow_api_key: str = ""
|
|
67
|
+
openrouter_api_key: str = ""
|
|
68
|
+
custom_api_key: str = ""
|
|
69
|
+
custom_base_url: str = ""
|
|
66
70
|
tavily_api_key: str = ""
|
|
67
71
|
|
|
68
72
|
# LLM Settings
|
|
@@ -213,6 +217,10 @@ _ENV_MAPPINGS = {
|
|
|
213
217
|
"openai_api_key": "OPENAI_API_KEY",
|
|
214
218
|
"nvidia_api_key": "NVIDIA_API_KEY",
|
|
215
219
|
"google_api_key": "GOOGLE_API_KEY",
|
|
220
|
+
"siliconflow_api_key": "SILICONFLOW_API_KEY",
|
|
221
|
+
"openrouter_api_key": "OPENROUTER_API_KEY",
|
|
222
|
+
"custom_api_key": "CUSTOM_API_KEY",
|
|
223
|
+
"custom_base_url": "CUSTOM_BASE_URL",
|
|
216
224
|
"tavily_api_key": "TAVILY_API_KEY",
|
|
217
225
|
"default_mode": "EVOSCIENTIST_DEFAULT_MODE",
|
|
218
226
|
"default_workdir": "EVOSCIENTIST_WORKSPACE_DIR",
|
|
@@ -281,5 +289,13 @@ def apply_config_to_env(config: EvoScientistConfig) -> None:
|
|
|
281
289
|
os.environ["NVIDIA_API_KEY"] = config.nvidia_api_key
|
|
282
290
|
if config.google_api_key and not os.environ.get("GOOGLE_API_KEY"):
|
|
283
291
|
os.environ["GOOGLE_API_KEY"] = config.google_api_key
|
|
292
|
+
if config.siliconflow_api_key and not os.environ.get("SILICONFLOW_API_KEY"):
|
|
293
|
+
os.environ["SILICONFLOW_API_KEY"] = config.siliconflow_api_key
|
|
294
|
+
if config.openrouter_api_key and not os.environ.get("OPENROUTER_API_KEY"):
|
|
295
|
+
os.environ["OPENROUTER_API_KEY"] = config.openrouter_api_key
|
|
296
|
+
if config.custom_api_key and not os.environ.get("CUSTOM_API_KEY"):
|
|
297
|
+
os.environ["CUSTOM_API_KEY"] = config.custom_api_key
|
|
298
|
+
if config.custom_base_url and not os.environ.get("CUSTOM_BASE_URL"):
|
|
299
|
+
os.environ["CUSTOM_BASE_URL"] = config.custom_base_url
|
|
284
300
|
if config.tavily_api_key and not os.environ.get("TAVILY_API_KEY"):
|
|
285
301
|
os.environ["TAVILY_API_KEY"] = config.tavily_api_key
|
|
@@ -8,6 +8,7 @@ from .models import (
|
|
|
8
8
|
MODELS,
|
|
9
9
|
DEFAULT_MODEL,
|
|
10
10
|
get_chat_model,
|
|
11
|
+
get_models_for_provider,
|
|
11
12
|
list_models,
|
|
12
13
|
get_model_info,
|
|
13
14
|
)
|
|
@@ -16,6 +17,7 @@ __all__ = [
|
|
|
16
17
|
"MODELS",
|
|
17
18
|
"DEFAULT_MODEL",
|
|
18
19
|
"get_chat_model",
|
|
20
|
+
"get_models_for_provider",
|
|
19
21
|
"list_models",
|
|
20
22
|
"get_model_info",
|
|
21
23
|
]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""LLM model configuration based on LangChain init_chat_model.
|
|
2
|
+
|
|
3
|
+
This module provides a unified interface for creating chat model instances
|
|
4
|
+
with support for multiple providers (Anthropic, OpenAI) and convenient
|
|
5
|
+
short names for common models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from langchain.chat_models import init_chat_model
|
|
14
|
+
|
|
15
|
+
_SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1"
|
|
16
|
+
_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
|
17
|
+
|
|
18
|
+
# Model registry: list of (short_name, model_id, provider)
|
|
19
|
+
# Allows same short_name across different providers.
|
|
20
|
+
_MODEL_ENTRIES: list[tuple[str, str, str]] = [
|
|
21
|
+
# Anthropic (ordered by capability)
|
|
22
|
+
("claude-opus-4-6", "claude-opus-4-6", "anthropic"),
|
|
23
|
+
("claude-opus-4-5", "claude-opus-4-5-20251101", "anthropic"),
|
|
24
|
+
("claude-sonnet-4-5", "claude-sonnet-4-5-20250929", "anthropic"),
|
|
25
|
+
("claude-haiku-4-5", "claude-haiku-4-5-20251001", "anthropic"),
|
|
26
|
+
# OpenAI
|
|
27
|
+
("gpt-5.2-codex", "gpt-5.2-codex", "openai"),
|
|
28
|
+
("gpt-5.2", "gpt-5.2-2025-12-11", "openai"),
|
|
29
|
+
("gpt-5.1", "gpt-5.1-2025-11-13", "openai"),
|
|
30
|
+
("gpt-5", "gpt-5-2025-08-07", "openai"),
|
|
31
|
+
("gpt-5-mini", "gpt-5-mini-2025-08-07", "openai"),
|
|
32
|
+
("gpt-5-nano", "gpt-5-nano-2025-08-07", "openai"),
|
|
33
|
+
# Google GenAI
|
|
34
|
+
("gemini-3-pro", "gemini-3-pro-preview", "google-genai"),
|
|
35
|
+
("gemini-3-flash", "gemini-3-flash-preview", "google-genai"),
|
|
36
|
+
("gemini-2.5-flash", "gemini-2.5-flash", "google-genai"),
|
|
37
|
+
("gemini-2.5-flash-lite", "gemini-2.5-flash-lite", "google-genai"),
|
|
38
|
+
("gemini-2.5-pro", "gemini-2.5-pro", "google-genai"),
|
|
39
|
+
# NVIDIA
|
|
40
|
+
("glm4.7", "z-ai/glm4.7", "nvidia"),
|
|
41
|
+
("deepseek-v3.2", "deepseek-ai/deepseek-v3.2", "nvidia"),
|
|
42
|
+
("deepseek-v3.1", "deepseek-ai/deepseek-v3.1-terminus", "nvidia"),
|
|
43
|
+
("kimi-k2.5", "moonshotai/kimi-k2.5", "nvidia"),
|
|
44
|
+
("kimi-k2-thinking", "moonshotai/kimi-k2-thinking", "nvidia"),
|
|
45
|
+
("minimax-m2.1", "minimaxai/minimax-m2.1", "nvidia"),
|
|
46
|
+
("step-3.5-flash", "stepfun-ai/step-3.5-flash", "nvidia"),
|
|
47
|
+
("nemotron-nano", "nvidia/nemotron-3-nano-30b-a3b", "nvidia"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Public dict for simple lookups (last entry wins for duplicate names).
|
|
51
|
+
# Use get_models_for_provider() for provider-aware lookups.
|
|
52
|
+
MODELS: dict[str, tuple[str, str]] = {
|
|
53
|
+
name: (model_id, provider) for name, model_id, provider in _MODEL_ENTRIES
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
DEFAULT_MODEL = "claude-sonnet-4-5"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_models_for_provider(provider: str) -> list[tuple[str, str]]:
|
|
60
|
+
"""Get all models for a specific provider.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
provider: Provider name (e.g., 'anthropic', 'openrouter').
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of (short_name, model_id) tuples for the provider.
|
|
67
|
+
"""
|
|
68
|
+
return [
|
|
69
|
+
(name, model_id)
|
|
70
|
+
for name, model_id, p in _MODEL_ENTRIES
|
|
71
|
+
if p == provider
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_chat_model(
|
|
76
|
+
model: str | None = None,
|
|
77
|
+
provider: str | None = None,
|
|
78
|
+
**kwargs: Any,
|
|
79
|
+
) -> Any:
|
|
80
|
+
"""Get a chat model instance.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
model: Model name (short name like 'claude-sonnet-4-5' or full ID
|
|
84
|
+
like 'claude-sonnet-4-5-20250929'). Defaults to DEFAULT_MODEL.
|
|
85
|
+
provider: Override the provider (e.g., 'anthropic', 'openai').
|
|
86
|
+
If not specified, inferred from model name or defaults to 'anthropic'.
|
|
87
|
+
**kwargs: Additional arguments passed to init_chat_model (e.g., temperature).
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A LangChain chat model instance.
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
>>> model = get_chat_model() # Uses default (claude-sonnet-4-5)
|
|
94
|
+
>>> model = get_chat_model("claude-opus-4-5") # Use short name
|
|
95
|
+
>>> model = get_chat_model("gpt-4o") # OpenAI model
|
|
96
|
+
>>> model = get_chat_model("claude-3-opus-20240229", provider="anthropic") # Full ID
|
|
97
|
+
"""
|
|
98
|
+
model = model or DEFAULT_MODEL
|
|
99
|
+
|
|
100
|
+
# Look up short name in registry (provider-aware)
|
|
101
|
+
model_id = None
|
|
102
|
+
if provider:
|
|
103
|
+
# Try exact match with provider first
|
|
104
|
+
for name, mid, p in _MODEL_ENTRIES:
|
|
105
|
+
if name == model and p == provider:
|
|
106
|
+
model_id = mid
|
|
107
|
+
break
|
|
108
|
+
if model_id is None and model in MODELS:
|
|
109
|
+
model_id, default_provider = MODELS[model]
|
|
110
|
+
provider = provider or default_provider
|
|
111
|
+
|
|
112
|
+
if model_id is None:
|
|
113
|
+
# Assume it's a full model ID
|
|
114
|
+
model_id = model
|
|
115
|
+
# Try to infer provider from model ID prefix
|
|
116
|
+
if provider is None:
|
|
117
|
+
if model_id.startswith(("claude-", "anthropic")):
|
|
118
|
+
provider = "anthropic"
|
|
119
|
+
elif model_id.startswith(("gpt-", "o1", "davinci", "text-")):
|
|
120
|
+
provider = "openai"
|
|
121
|
+
elif model_id.startswith("gemini"):
|
|
122
|
+
provider = "google-genai"
|
|
123
|
+
elif "/" in model_id:
|
|
124
|
+
provider = "nvidia"
|
|
125
|
+
else:
|
|
126
|
+
provider = "anthropic" # Default fallback
|
|
127
|
+
|
|
128
|
+
# SiliconFlow / OpenRouter / Custom → route through OpenAI provider with base_url
|
|
129
|
+
_is_third_party = provider in ("siliconflow", "openrouter", "custom")
|
|
130
|
+
if provider == "custom":
|
|
131
|
+
base_url = os.environ.get("CUSTOM_BASE_URL", "")
|
|
132
|
+
if base_url:
|
|
133
|
+
kwargs["base_url"] = base_url
|
|
134
|
+
api_key = os.environ.get("CUSTOM_API_KEY", "")
|
|
135
|
+
if api_key:
|
|
136
|
+
kwargs["api_key"] = api_key
|
|
137
|
+
provider = "openai"
|
|
138
|
+
elif provider == "siliconflow":
|
|
139
|
+
kwargs["base_url"] = _SILICONFLOW_BASE_URL
|
|
140
|
+
api_key = os.environ.get("SILICONFLOW_API_KEY", "")
|
|
141
|
+
if api_key:
|
|
142
|
+
kwargs["api_key"] = api_key
|
|
143
|
+
provider = "openai"
|
|
144
|
+
elif provider == "openrouter":
|
|
145
|
+
kwargs["base_url"] = _OPENROUTER_BASE_URL
|
|
146
|
+
api_key = os.environ.get("OPENROUTER_API_KEY", "")
|
|
147
|
+
if api_key:
|
|
148
|
+
kwargs["api_key"] = api_key
|
|
149
|
+
provider = "openai"
|
|
150
|
+
|
|
151
|
+
# Auto-enable thinking for Anthropic models
|
|
152
|
+
if provider == "anthropic" and "thinking" not in kwargs:
|
|
153
|
+
if model_id.startswith("claude-opus-4-6"):
|
|
154
|
+
kwargs["thinking"] = {"type": "adaptive"}
|
|
155
|
+
else:
|
|
156
|
+
kwargs["thinking"] = {"type": "enabled", "budget_tokens": 2000}
|
|
157
|
+
|
|
158
|
+
# Auto-enable reasoning for OpenAI models (not for third-party routed)
|
|
159
|
+
if provider == "openai" and not _is_third_party and "reasoning" not in kwargs:
|
|
160
|
+
kwargs["reasoning"] = {"effort": "medium", "summary": "auto"}
|
|
161
|
+
|
|
162
|
+
# Auto-enable thinking visibility for Google GenAI models
|
|
163
|
+
if provider == "google-genai":
|
|
164
|
+
kwargs.setdefault("include_thoughts", True)
|
|
165
|
+
|
|
166
|
+
return init_chat_model(model=model_id, model_provider=provider, **kwargs)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def list_models() -> list[str]:
|
|
170
|
+
"""List all available model short names.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of unique model short names that can be passed to get_chat_model().
|
|
174
|
+
"""
|
|
175
|
+
seen = set()
|
|
176
|
+
result = []
|
|
177
|
+
for name, _, _ in _MODEL_ENTRIES:
|
|
178
|
+
if name not in seen:
|
|
179
|
+
seen.add(name)
|
|
180
|
+
result.append(name)
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_model_info(model: str) -> tuple[str, str] | None:
|
|
185
|
+
"""Get the (model_id, provider) tuple for a short name.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
model: Short model name.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Tuple of (model_id, provider) or None if not found.
|
|
192
|
+
"""
|
|
193
|
+
return MODELS.get(model)
|
|
@@ -302,8 +302,7 @@ def _merge_memory(existing_md: str, extracted: dict[str, Any]) -> str:
|
|
|
302
302
|
if value and value != "null":
|
|
303
303
|
# Replace the line "- **Label**: ..." with new value
|
|
304
304
|
pattern = rf"(- \*\*{label}\*\*: ).*"
|
|
305
|
-
|
|
306
|
-
result = re.sub(pattern, replacement, result)
|
|
305
|
+
result = re.sub(pattern, lambda m: m.group(1) + value, result)
|
|
307
306
|
|
|
308
307
|
# --- Research Preferences ---
|
|
309
308
|
prefs = extracted.get("research_preferences")
|
|
@@ -320,8 +319,7 @@ def _merge_memory(existing_md: str, extracted: dict[str, Any]) -> str:
|
|
|
320
319
|
value = prefs.get(key)
|
|
321
320
|
if value and value != "null":
|
|
322
321
|
pattern = rf"(- \*\*{label}\*\*: ).*"
|
|
323
|
-
|
|
324
|
-
result = re.sub(pattern, replacement, result)
|
|
322
|
+
result = re.sub(pattern, lambda m: m.group(1) + value, result)
|
|
325
323
|
|
|
326
324
|
# --- Experiment History (append) ---
|
|
327
325
|
exp = extracted.get("experiment_conclusion")
|