vamp-cli 0.1.0__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.
- vamp_cli/__init__.py +0 -0
- vamp_cli/agent/__init__.py +0 -0
- vamp_cli/agent/context_sync.py +72 -0
- vamp_cli/agent/graph.py +159 -0
- vamp_cli/agent/instructions.py +18 -0
- vamp_cli/agent/nodes.py +375 -0
- vamp_cli/agent/roles.py +8 -0
- vamp_cli/agent/runtime/__init__.py +0 -0
- vamp_cli/agent/runtime/runtime.py +133 -0
- vamp_cli/agent/state.py +55 -0
- vamp_cli/agent/status.py +17 -0
- vamp_cli/config/__init__.py +0 -0
- vamp_cli/config/loader.py +152 -0
- vamp_cli/config/schema.py +82 -0
- vamp_cli/llm/__init__.py +0 -0
- vamp_cli/llm/adapter.py +52 -0
- vamp_cli/llm/adapter_registry.py +26 -0
- vamp_cli/llm/debug_callbacks.py +18 -0
- vamp_cli/llm/router.py +247 -0
- vamp_cli/llm/usage.py +170 -0
- vamp_cli/logging_setup/logging_setup.py +37 -0
- vamp_cli/main.py +1023 -0
- vamp_cli/persistence/__init__.py +29 -0
- vamp_cli/persistence/compaction.py +372 -0
- vamp_cli/persistence/emitter.py +371 -0
- vamp_cli/persistence/event_log.py +303 -0
- vamp_cli/persistence/fork.py +81 -0
- vamp_cli/persistence/loader.py +616 -0
- vamp_cli/persistence/transcript.py +160 -0
- vamp_cli/prompts/prompts.py +182 -0
- vamp_cli/sandbox/__init__.py +0 -0
- vamp_cli/sandbox/base.py +22 -0
- vamp_cli/sandbox/config.py +16 -0
- vamp_cli/sandbox/docker.py +293 -0
- vamp_cli/sandbox/manager.py +31 -0
- vamp_cli/sandbox/result.py +14 -0
- vamp_cli/sandbox/terminal.py +151 -0
- vamp_cli/tools/__init__.py +0 -0
- vamp_cli/tools/base.py +44 -0
- vamp_cli/tools/diagnostic.py +120 -0
- vamp_cli/tools/execute_cmd.py +16 -0
- vamp_cli/tools/files.py +652 -0
- vamp_cli/tools/registry.py +163 -0
- vamp_cli/tools/tools_classes.py +622 -0
- vamp_cli/utils/__init__.py +0 -0
- vamp_cli/utils/error_codes.py +7 -0
- vamp_cli/utils/ignore.py +36 -0
- vamp_cli/utils/sha_256.py +32 -0
- vamp_cli/utils/token_tracker.py +31 -0
- vamp_cli/workspace/__init__.py +0 -0
- vamp_cli/workspace/workspace.py +16 -0
- vamp_cli-0.1.0.dist-info/METADATA +317 -0
- vamp_cli-0.1.0.dist-info/RECORD +57 -0
- vamp_cli-0.1.0.dist-info/WHEEL +5 -0
- vamp_cli-0.1.0.dist-info/entry_points.txt +2 -0
- vamp_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- vamp_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Sequence
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from langchain_core.runnables import Runnable
|
|
8
|
+
|
|
9
|
+
from vamp_cli.config.schema import AppConfig, ProfileConfig
|
|
10
|
+
from vamp_cli.llm.router import ModelRouter
|
|
11
|
+
from vamp_cli.tools.registry import ToolRegistry
|
|
12
|
+
from vamp_cli.prompts.prompts import PromptStore
|
|
13
|
+
from vamp_cli.agent.roles import AgentRole
|
|
14
|
+
from vamp_cli.tools.base import Tools
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class AgentProfile:
|
|
20
|
+
role: str
|
|
21
|
+
model: Runnable[Any, Any]
|
|
22
|
+
tools: dict[str, Tools] # {name: Tools} - for execution dispatch
|
|
23
|
+
tool_schemas: list[dict] # OpenAI-format - for giving tool list to llm model
|
|
24
|
+
system_prompt: str
|
|
25
|
+
context_window: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AgentRuntime:
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
config: AppConfig,
|
|
33
|
+
model_router: ModelRouter,
|
|
34
|
+
tool_registry: ToolRegistry,
|
|
35
|
+
prompt_store: PromptStore,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._config = config
|
|
38
|
+
self._model_router = model_router
|
|
39
|
+
self._tool_registry = tool_registry
|
|
40
|
+
self._prompt_store = prompt_store
|
|
41
|
+
self._profile_cache: dict[str, AgentProfile] = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve(self, role: AgentRole) -> AgentProfile:
|
|
45
|
+
|
|
46
|
+
# check cache hit
|
|
47
|
+
if role.value in self._profile_cache:
|
|
48
|
+
return self._profile_cache[role.value]
|
|
49
|
+
|
|
50
|
+
profile = self._build_profile(role=role)
|
|
51
|
+
|
|
52
|
+
# cache the profile
|
|
53
|
+
self._profile_cache[role.value] = profile
|
|
54
|
+
|
|
55
|
+
return profile
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _build_profile(self, role: AgentRole) -> AgentProfile:
|
|
59
|
+
|
|
60
|
+
profile_cfg = self._resolve_profile(role)
|
|
61
|
+
|
|
62
|
+
# Tool objects, keyed by name - for execution.
|
|
63
|
+
tools_by_name = self._tool_registry.get_tools(profile_cfg.tools)
|
|
64
|
+
|
|
65
|
+
# OpenAI-format schemas - for binding into the router.
|
|
66
|
+
tool_schemas = self._tool_registry.get_openai_tools(profile_cfg.tools)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
system_prompt = self._prompt_store.load(profile_cfg.prompt)
|
|
70
|
+
|
|
71
|
+
model = self._model_router.get(
|
|
72
|
+
profile_cfg.model,
|
|
73
|
+
tools=tool_schemas, # openai compatiable tool schema dict
|
|
74
|
+
usage_context=role.value,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
context_window_size = self._config.models[profile_cfg.model].capabilities.context_window
|
|
78
|
+
|
|
79
|
+
profile = AgentProfile(
|
|
80
|
+
role=role.value,
|
|
81
|
+
model=model,
|
|
82
|
+
tools=tools_by_name,
|
|
83
|
+
tool_schemas=tool_schemas,
|
|
84
|
+
system_prompt=system_prompt,
|
|
85
|
+
context_window=context_window_size
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return profile
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _resolve_profile(self, role: AgentRole)-> ProfileConfig:
|
|
92
|
+
|
|
93
|
+
profiles = self._config.profiles
|
|
94
|
+
|
|
95
|
+
if role.value in profiles:
|
|
96
|
+
return profiles[role.value]
|
|
97
|
+
|
|
98
|
+
if role.value not in profiles:
|
|
99
|
+
if role.value == "coder":
|
|
100
|
+
return profiles["default_coder"]
|
|
101
|
+
if role.value == "planner":
|
|
102
|
+
return profiles["default_planner"]
|
|
103
|
+
|
|
104
|
+
raise KeyError(
|
|
105
|
+
f"Role '{role}' is not configured and no 'default_{role.value}' profile exists"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
'''
|
|
114
|
+
at call site:
|
|
115
|
+
|
|
116
|
+
tool_calls = response.tool_calls
|
|
117
|
+
|
|
118
|
+
for tc in tool_calls:
|
|
119
|
+
|
|
120
|
+
# openai raw tool call
|
|
121
|
+
name = tc["function"]["name"]
|
|
122
|
+
args = json.loads(tc["function"]["arguments"])
|
|
123
|
+
|
|
124
|
+
tool = profile.tools[name] # Dict[str, Tools]
|
|
125
|
+
result = tool.run(**args)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def normalize_tool_call(tc: dict) -> tuple[str, dict]:
|
|
129
|
+
if "function" in tc: # OpenAI raw
|
|
130
|
+
return tc["function"]["name"], json.loads(tc["function"]["arguments"])
|
|
131
|
+
return tc["name"], tc["args"] # LangChain
|
|
132
|
+
|
|
133
|
+
'''
|
vamp_cli/agent/state.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import operator
|
|
3
|
+
from typing import TypedDict, Annotated, Literal
|
|
4
|
+
|
|
5
|
+
from langchain_core.messages import BaseMessage
|
|
6
|
+
from langgraph.graph.message import add_messages
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TodoItem(TypedDict):
|
|
11
|
+
id: str
|
|
12
|
+
description: str
|
|
13
|
+
status: Literal[
|
|
14
|
+
"pending",
|
|
15
|
+
"in_progress",
|
|
16
|
+
"completed",
|
|
17
|
+
"blocked",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentState(TypedDict):
|
|
22
|
+
|
|
23
|
+
# messages: Annotated[list[BaseMessage], add_messages]
|
|
24
|
+
planner_messages: Annotated[
|
|
25
|
+
list[BaseMessage],
|
|
26
|
+
add_messages,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
requires_execution: bool
|
|
30
|
+
|
|
31
|
+
coder_messages: Annotated[
|
|
32
|
+
list[BaseMessage],
|
|
33
|
+
add_messages,
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
task: str
|
|
37
|
+
|
|
38
|
+
mode: Literal["normal", "plan"]
|
|
39
|
+
|
|
40
|
+
plan: str | None # only generated in plan mode
|
|
41
|
+
|
|
42
|
+
todo: list[TodoItem]
|
|
43
|
+
|
|
44
|
+
iteration: int
|
|
45
|
+
|
|
46
|
+
# tool-approval pipeline
|
|
47
|
+
_pending_tool_channel: str | None
|
|
48
|
+
_pending_tool_calls: list[dict]
|
|
49
|
+
_dangerous_tool_call_ids: list[str]
|
|
50
|
+
_approved_tool_call_ids: list[str]
|
|
51
|
+
_denied_tool_call_ids: list[str]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
vamp_cli/agent/status.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentStatus(str, Enum):
|
|
7
|
+
PLANNING = "planning"
|
|
8
|
+
EXECUTING = "executing"
|
|
9
|
+
VERIFYING = "verifying"
|
|
10
|
+
REPAIRING = "repairing"
|
|
11
|
+
COMPLETED = "completed"
|
|
12
|
+
FAILED = "failed"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FeedbackResponse(BaseModel):
|
|
16
|
+
approved: bool
|
|
17
|
+
reason: Optional[str] = None
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from vamp_cli.config.schema import AppConfig, ModelConfig
|
|
11
|
+
|
|
12
|
+
DEFAULT_CONFIG_DIR = Path(__file__).resolve().parent
|
|
13
|
+
DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_DIR / "default_config.yaml"
|
|
14
|
+
|
|
15
|
+
CONFIG_PATH = Path.home() / ".vamp/config.yaml"
|
|
16
|
+
|
|
17
|
+
class ConfigError(ValueError):
|
|
18
|
+
"""Raised when configuration is structurally invalid or incomplete."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_config() -> AppConfig:
|
|
22
|
+
"""Load, validate, resolve secrets, and cross-check the YAML config.
|
|
23
|
+
|
|
24
|
+
This is the only place in the application that reads config.yaml.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
config_path = CONFIG_PATH
|
|
28
|
+
|
|
29
|
+
if not config_path.exists():
|
|
30
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
with open(DEFAULT_CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
33
|
+
content = f.read()
|
|
34
|
+
|
|
35
|
+
with open(config_path, "x", encoding="utf-8") as f:
|
|
36
|
+
f.write(content)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if not config_path.is_file():
|
|
40
|
+
raise ConfigError(f"Config file does not exist: {config_path}")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
with config_path.open("r", encoding="utf-8") as fh:
|
|
44
|
+
raw: Any = yaml.safe_load(fh) or {}
|
|
45
|
+
except yaml.YAMLError as exc:
|
|
46
|
+
raise ConfigError(f"Invalid YAML in {config_path}: {exc}") from exc
|
|
47
|
+
except OSError as exc:
|
|
48
|
+
raise ConfigError(f"Unable to read config {config_path}: {exc}") from exc
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
config = AppConfig.model_validate(raw)
|
|
52
|
+
except ValidationError as exc:
|
|
53
|
+
raise ConfigError(f"Invalid configuration in {config_path}:\n{exc}") from exc
|
|
54
|
+
|
|
55
|
+
_resolve_api_keys(config)
|
|
56
|
+
_validate_references(config)
|
|
57
|
+
return config
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_api_key(cfg: ModelConfig) -> str | None:
|
|
61
|
+
"""Return a model's already-resolved API key."""
|
|
62
|
+
|
|
63
|
+
return cfg.api_key
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _resolve_api_keys(config: AppConfig) -> None:
|
|
67
|
+
"""Resolve API key environment variables into the in-memory config."""
|
|
68
|
+
|
|
69
|
+
for name, cfg in config.models.items():
|
|
70
|
+
if not cfg.api_key_env:
|
|
71
|
+
continue
|
|
72
|
+
try:
|
|
73
|
+
value = os.getenv(cfg.api_key_env)
|
|
74
|
+
if not value:
|
|
75
|
+
# print(
|
|
76
|
+
# ConfigError(
|
|
77
|
+
# f"Model '{name}' requires environment variable "
|
|
78
|
+
# f"'{cfg.api_key_env}', but it is not set."
|
|
79
|
+
# )
|
|
80
|
+
# )
|
|
81
|
+
cfg.api_key = "EXAMPLEMODEL_API_KEY"
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
cfg.api_key = value
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _validate_references(config: AppConfig) -> None:
|
|
91
|
+
"""Validate model references, profile references, and fallback cycles."""
|
|
92
|
+
|
|
93
|
+
model_names = set(config.models)
|
|
94
|
+
|
|
95
|
+
if not model_names:
|
|
96
|
+
raise ConfigError("At least one model must be configured.")
|
|
97
|
+
|
|
98
|
+
if config.profiles and any(p not in config.profiles for p in config.default_profile):
|
|
99
|
+
raise ConfigError(
|
|
100
|
+
f"default_profile '{config.default_profile}' is not defined in profiles."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for profile_name, profile in config.profiles.items():
|
|
104
|
+
if profile.model not in model_names:
|
|
105
|
+
raise ConfigError(
|
|
106
|
+
f"Profile '{profile_name}' references unknown model "
|
|
107
|
+
f"'{profile.model}'."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
for model_name, model in config.models.items():
|
|
111
|
+
for fallback in model.fallbacks:
|
|
112
|
+
if fallback not in model_names:
|
|
113
|
+
raise ConfigError(
|
|
114
|
+
f"Model '{model_name}' references unknown fallback '{fallback}'."
|
|
115
|
+
)
|
|
116
|
+
if fallback == model_name:
|
|
117
|
+
raise ConfigError(
|
|
118
|
+
f"Model '{model_name}' cannot use itself as a fallback."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
_check_fallback_cycles(config)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _check_fallback_cycles(config: AppConfig) -> None:
|
|
125
|
+
visiting: set[str] = set()
|
|
126
|
+
visited: set[str] = set()
|
|
127
|
+
path: list[str] = [] # tracks the exact order of traversal
|
|
128
|
+
|
|
129
|
+
def visit(model_name: str) -> None:
|
|
130
|
+
if model_name in visiting:
|
|
131
|
+
cycle_start_idx = path.index(model_name)
|
|
132
|
+
cycle_path = path[cycle_start_idx:] + [model_name]
|
|
133
|
+
path_str = " -> ".join(cycle_path)
|
|
134
|
+
raise ConfigError(
|
|
135
|
+
f"Fallback cycle detected involving model '{model_name}'."
|
|
136
|
+
f"Fallback cycle: {path_str}"
|
|
137
|
+
)
|
|
138
|
+
if model_name in visited:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
visiting.add(model_name)
|
|
142
|
+
path.append(model_name) # push to history stack
|
|
143
|
+
|
|
144
|
+
for fallback in config.models[model_name].fallbacks:
|
|
145
|
+
visit(fallback)
|
|
146
|
+
|
|
147
|
+
path.pop() # pop from history stack
|
|
148
|
+
visiting.remove(model_name)
|
|
149
|
+
visited.add(model_name)
|
|
150
|
+
|
|
151
|
+
for model_name in config.models:
|
|
152
|
+
visit(model_name)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModelCapabilities(BaseModel):
|
|
9
|
+
"""Declarative capabilities used by higher layers before invocation."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(extra="allow")
|
|
12
|
+
|
|
13
|
+
supports_tools: bool = True
|
|
14
|
+
supports_parallel_tools: bool = True
|
|
15
|
+
supports_streaming: bool = True
|
|
16
|
+
|
|
17
|
+
supports_reasoning: bool = False
|
|
18
|
+
reasoning_param: str | None = None # ex: "reasoning_effort" or "thinking"
|
|
19
|
+
reasoning_value: Any | None = None # ex: "high" or {"type": "enabled", "budget_tokens": 4000}
|
|
20
|
+
|
|
21
|
+
context_window: int = 128_000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ModelConfig(BaseModel):
|
|
25
|
+
"""Configuration for one logical model name."""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(extra="forbid")
|
|
28
|
+
|
|
29
|
+
model: str
|
|
30
|
+
api_base: str | None = None
|
|
31
|
+
api_key_env: str | None = None
|
|
32
|
+
|
|
33
|
+
# Resolved by config.loader.py from api_key_env.
|
|
34
|
+
# It is excluded from normal serialization and displayed masked by SecretStr.
|
|
35
|
+
api_key: str | None = Field(default=None, repr=False)
|
|
36
|
+
|
|
37
|
+
params: dict[str, Any] = Field(default_factory=dict)
|
|
38
|
+
fallbacks: list[str] = Field(default_factory=list)
|
|
39
|
+
adapter: str = "default"
|
|
40
|
+
capabilities: ModelCapabilities = Field(default_factory=ModelCapabilities)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ProfileConfig(BaseModel):
|
|
44
|
+
"""Agent profile configuration consumed later by AgentRuntime."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(extra="forbid")
|
|
47
|
+
|
|
48
|
+
model: str
|
|
49
|
+
tools: list[str] = Field(default_factory=list)
|
|
50
|
+
prompt: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AppConfig(BaseModel):
|
|
54
|
+
"""Validated application configuration loaded from YAML."""
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(extra="forbid")
|
|
57
|
+
|
|
58
|
+
models: dict[str, ModelConfig]
|
|
59
|
+
profiles: dict[str, ProfileConfig] = Field(default_factory=dict)
|
|
60
|
+
default_profile: list[str] = ["default_coder", "default_planner"]
|
|
61
|
+
|
|
62
|
+
@model_validator(mode="after")
|
|
63
|
+
def _check_references(self) -> "AppConfig":
|
|
64
|
+
|
|
65
|
+
# Ensure default_profile list is not empty
|
|
66
|
+
if not self.default_profile:
|
|
67
|
+
raise ValueError("config must define at least one item in default_profile")
|
|
68
|
+
|
|
69
|
+
# Check that every item in default_profile list actually exists under profiles
|
|
70
|
+
for p in self.default_profile:
|
|
71
|
+
if p not in self.profiles:
|
|
72
|
+
raise ValueError(f"default_profile item '{p}' is not defined in profiles.")
|
|
73
|
+
|
|
74
|
+
# Check profile models match defined models
|
|
75
|
+
for name, profile in self.profiles.items():
|
|
76
|
+
if profile.model not in self.models:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"profile '{name}' uses unknown model '{profile.model}'. "
|
|
79
|
+
f"Defined models: {sorted(self.models)}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return self
|
vamp_cli/llm/__init__.py
ADDED
|
File without changes
|
vamp_cli/llm/adapter.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
from langchain_core.runnables import Runnable
|
|
6
|
+
|
|
7
|
+
from vamp_cli.config.schema import ModelConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ModelAdapter(Protocol):
|
|
12
|
+
def adapt(
|
|
13
|
+
self,
|
|
14
|
+
runnable: Runnable[Any, Any],
|
|
15
|
+
*,
|
|
16
|
+
model_name: str,
|
|
17
|
+
config: ModelConfig,
|
|
18
|
+
) -> Runnable[Any, Any]:
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PassThroughAdapter(ModelAdapter):
|
|
23
|
+
"""Default adapter for models with no special handling."""
|
|
24
|
+
|
|
25
|
+
def adapt(
|
|
26
|
+
self,
|
|
27
|
+
runnable: Runnable[Any, Any],
|
|
28
|
+
*,
|
|
29
|
+
model_name: str,
|
|
30
|
+
config: ModelConfig,
|
|
31
|
+
) -> Runnable[Any, Any]:
|
|
32
|
+
return runnable
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReasoningAdapter(ModelAdapter):
|
|
36
|
+
"""Injects provider-specific reasoning/thinking parameters into the runnable."""
|
|
37
|
+
|
|
38
|
+
def adapt(
|
|
39
|
+
self,
|
|
40
|
+
runnable: Runnable[Any, Any],
|
|
41
|
+
*,
|
|
42
|
+
model_name: str,
|
|
43
|
+
config: ModelConfig,
|
|
44
|
+
) -> Runnable[Any, Any]:
|
|
45
|
+
caps = config.capabilities
|
|
46
|
+
|
|
47
|
+
# If the model configuration has reasoning enabled and specifies parameter structures
|
|
48
|
+
if caps.supports_reasoning and caps.reasoning_param:
|
|
49
|
+
kwargs = {caps.reasoning_param: caps.reasoning_value}
|
|
50
|
+
return runnable.bind(**kwargs)
|
|
51
|
+
|
|
52
|
+
return runnable
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from vamp_cli.llm.adapter import (
|
|
2
|
+
ModelAdapter,
|
|
3
|
+
PassThroughAdapter,
|
|
4
|
+
ReasoningAdapter
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AdapterRegistry:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
adapters: dict[str, ModelAdapter] | None = None,
|
|
12
|
+
) -> None:
|
|
13
|
+
self._adapters = {
|
|
14
|
+
"reasoning": ReasoningAdapter(),
|
|
15
|
+
"default": PassThroughAdapter(),
|
|
16
|
+
**(adapters or {}),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def get(self, name: str) -> ModelAdapter:
|
|
20
|
+
try:
|
|
21
|
+
return self._adapters[name]
|
|
22
|
+
except KeyError:
|
|
23
|
+
raise KeyError(
|
|
24
|
+
f"Unknown model adapter '{name}'. "
|
|
25
|
+
f"Available adapters: {sorted(self._adapters)}"
|
|
26
|
+
) from None
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import json
|
|
3
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class DumpPayloadCallback(BaseCallbackHandler):
|
|
8
|
+
def on_llm_start(self, serialized, prompts, **kwargs):
|
|
9
|
+
log.info("=== LLM START ===")
|
|
10
|
+
log.info("serialized=%s", json.dumps(serialized, default=str)[:2000])
|
|
11
|
+
log.info("prompts=%s", prompts)
|
|
12
|
+
|
|
13
|
+
def on_chat_model_start(self, serialized, messages, **kwargs):
|
|
14
|
+
log.info("=== CHAT START ===")
|
|
15
|
+
log.info("messages=%s", messages)
|
|
16
|
+
log.info("invocation_params=%s", kwargs.get("invocation_params"))
|
|
17
|
+
log.info("options=%s", kwargs.get("options"))
|
|
18
|
+
log.info("metadata=%s", kwargs.get("metadata"))
|