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.
Files changed (83) hide show
  1. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/_app.py +1 -1
  2. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/commands.py +14 -14
  3. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/interactive.py +13 -4
  4. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/config.py +16 -0
  5. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/llm/__init__.py +2 -0
  6. evoscientist-0.0.1.dev7/EvoScientist/llm/models.py +193 -0
  7. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/memory.py +2 -4
  8. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/onboard.py +271 -47
  9. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/sessions.py +17 -4
  10. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/PKG-INFO +1 -1
  11. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/SOURCES.txt +1 -0
  12. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/PKG-INFO +1 -1
  13. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/pyproject.toml +1 -1
  14. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_llm.py +37 -46
  15. evoscientist-0.0.1.dev7/tests/test_memory_merge.py +73 -0
  16. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_onboard.py +44 -0
  17. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_sessions.py +53 -0
  18. evoscientist-0.0.1.dev6/EvoScientist/llm/models.py +0 -115
  19. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/EvoScientist.py +0 -0
  20. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/__init__.py +0 -0
  21. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/__main__.py +0 -0
  22. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/backends.py +0 -0
  23. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/__init__.py +0 -0
  24. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/base.py +0 -0
  25. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/__init__.py +0 -0
  26. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/channel_rpc.py +0 -0
  27. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/probe.py +0 -0
  28. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/rpc_client.py +0 -0
  29. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/serve.py +0 -0
  30. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/channels/imessage/targets.py +0 -0
  31. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/__init__.py +0 -0
  32. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/agent.py +0 -0
  33. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/channel.py +0 -0
  34. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/mcp_ui.py +0 -0
  35. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/cli/skills_cmd.py +0 -0
  36. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/mcp/__init__.py +0 -0
  37. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/mcp/client.py +0 -0
  38. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/middleware.py +0 -0
  39. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/paths.py +0 -0
  40. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/prompts.py +0 -0
  41. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/find-skills/SKILL.md +0 -0
  42. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/SKILL.md +0 -0
  43. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/references/output-patterns.md +0 -0
  44. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/references/workflows.md +0 -0
  45. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/scripts/init_skill.py +0 -0
  46. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/scripts/package_skill.py +0 -0
  47. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/skills/skill-creator/scripts/quick_validate.py +0 -0
  48. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/__init__.py +0 -0
  49. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/display.py +0 -0
  50. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/emitter.py +0 -0
  51. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/events.py +0 -0
  52. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/formatter.py +0 -0
  53. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/state.py +0 -0
  54. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/tracker.py +0 -0
  55. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/stream/utils.py +0 -0
  56. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/subagent.yaml +0 -0
  57. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/__init__.py +0 -0
  58. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/search.py +0 -0
  59. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/skill_manager.py +0 -0
  60. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/skills_manager.py +0 -0
  61. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/tools/think.py +0 -0
  62. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist/utils.py +0 -0
  63. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/dependency_links.txt +0 -0
  64. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/entry_points.txt +0 -0
  65. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/requires.txt +0 -0
  66. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/EvoScientist.egg-info/top_level.txt +0 -0
  67. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/LICENSE +0 -0
  68. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/README.md +0 -0
  69. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/setup.cfg +0 -0
  70. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_backends.py +0 -0
  71. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_cli_run_name.py +0 -0
  72. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_config.py +0 -0
  73. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_event_loop.py +0 -0
  74. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_imports.py +0 -0
  75. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_mcp_client.py +0 -0
  76. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_prompts.py +0 -0
  77. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_skills_manager.py +0 -0
  78. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_emitter.py +0 -0
  79. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_events.py +0 -0
  80. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_state.py +0 -0
  81. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_tracker.py +0 -0
  82. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_stream_utils.py +0 -0
  83. {evoscientist-0.0.1.dev6 → evoscientist-0.0.1.dev7}/tests/test_tools.py +0 -0
@@ -14,7 +14,7 @@ app.add_typer(config_app, name="config")
14
14
 
15
15
  # MCP subcommand group
16
16
  _MCP_HELP = """\
17
- Configure and manage MCP servers.
17
+ Configure and manage MCP servers
18
18
 
19
19
  Examples:
20
20
  # Add a local MCP server (stdio auto-detected):
@@ -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=20, include_message_count=True, include_preview=True,
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=10, include_message_count=True, include_preview=True,
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
- selected = questionary.select(
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
- ).ask()
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
- replacement = rf"\g<1>{value}"
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
- replacement = rf"\g<1>{value}"
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")