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.
Files changed (57) hide show
  1. vamp_cli/__init__.py +0 -0
  2. vamp_cli/agent/__init__.py +0 -0
  3. vamp_cli/agent/context_sync.py +72 -0
  4. vamp_cli/agent/graph.py +159 -0
  5. vamp_cli/agent/instructions.py +18 -0
  6. vamp_cli/agent/nodes.py +375 -0
  7. vamp_cli/agent/roles.py +8 -0
  8. vamp_cli/agent/runtime/__init__.py +0 -0
  9. vamp_cli/agent/runtime/runtime.py +133 -0
  10. vamp_cli/agent/state.py +55 -0
  11. vamp_cli/agent/status.py +17 -0
  12. vamp_cli/config/__init__.py +0 -0
  13. vamp_cli/config/loader.py +152 -0
  14. vamp_cli/config/schema.py +82 -0
  15. vamp_cli/llm/__init__.py +0 -0
  16. vamp_cli/llm/adapter.py +52 -0
  17. vamp_cli/llm/adapter_registry.py +26 -0
  18. vamp_cli/llm/debug_callbacks.py +18 -0
  19. vamp_cli/llm/router.py +247 -0
  20. vamp_cli/llm/usage.py +170 -0
  21. vamp_cli/logging_setup/logging_setup.py +37 -0
  22. vamp_cli/main.py +1023 -0
  23. vamp_cli/persistence/__init__.py +29 -0
  24. vamp_cli/persistence/compaction.py +372 -0
  25. vamp_cli/persistence/emitter.py +371 -0
  26. vamp_cli/persistence/event_log.py +303 -0
  27. vamp_cli/persistence/fork.py +81 -0
  28. vamp_cli/persistence/loader.py +616 -0
  29. vamp_cli/persistence/transcript.py +160 -0
  30. vamp_cli/prompts/prompts.py +182 -0
  31. vamp_cli/sandbox/__init__.py +0 -0
  32. vamp_cli/sandbox/base.py +22 -0
  33. vamp_cli/sandbox/config.py +16 -0
  34. vamp_cli/sandbox/docker.py +293 -0
  35. vamp_cli/sandbox/manager.py +31 -0
  36. vamp_cli/sandbox/result.py +14 -0
  37. vamp_cli/sandbox/terminal.py +151 -0
  38. vamp_cli/tools/__init__.py +0 -0
  39. vamp_cli/tools/base.py +44 -0
  40. vamp_cli/tools/diagnostic.py +120 -0
  41. vamp_cli/tools/execute_cmd.py +16 -0
  42. vamp_cli/tools/files.py +652 -0
  43. vamp_cli/tools/registry.py +163 -0
  44. vamp_cli/tools/tools_classes.py +622 -0
  45. vamp_cli/utils/__init__.py +0 -0
  46. vamp_cli/utils/error_codes.py +7 -0
  47. vamp_cli/utils/ignore.py +36 -0
  48. vamp_cli/utils/sha_256.py +32 -0
  49. vamp_cli/utils/token_tracker.py +31 -0
  50. vamp_cli/workspace/__init__.py +0 -0
  51. vamp_cli/workspace/workspace.py +16 -0
  52. vamp_cli-0.1.0.dist-info/METADATA +317 -0
  53. vamp_cli-0.1.0.dist-info/RECORD +57 -0
  54. vamp_cli-0.1.0.dist-info/WHEEL +5 -0
  55. vamp_cli-0.1.0.dist-info/entry_points.txt +2 -0
  56. vamp_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  57. vamp_cli-0.1.0.dist-info/top_level.txt +1 -0
vamp_cli/llm/router.py ADDED
@@ -0,0 +1,247 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import threading
5
+ from dataclasses import dataclass
6
+ from typing import Any, Sequence
7
+
8
+ from langchain_core.runnables import Runnable
9
+ # from langchain_litellm import ChatLiteLLM
10
+
11
+ from vamp_cli.config.loader import get_api_key
12
+ from vamp_cli.config.schema import AppConfig, ModelCapabilities, ModelConfig
13
+ from vamp_cli.llm.adapter_registry import AdapterRegistry
14
+ from vamp_cli.llm.usage import UsageTracker, UsageSnapshot, _UsageCallback
15
+ from vamp_cli.llm.debug_callbacks import DumpPayloadCallback
16
+
17
+ # log = logging.getLogger(__name__)
18
+
19
+
20
+ # Arguments that ChatLiteLLM accepts directly. Everything else in params is
21
+ # passed through as model_kwargs for LiteLLM/provider-specific parameters.
22
+ _DIRECT_PARAMS = {
23
+ "temperature",
24
+ "max_tokens",
25
+ "top_p",
26
+ "request_timeout",
27
+ "max_retries",
28
+ "streaming",
29
+ }
30
+
31
+
32
+ class ModelRouter:
33
+ """Builds, binds, composes, and caches configured chat model runnables.
34
+
35
+ Public flow:
36
+
37
+ router.get("main", tools=[...])
38
+
39
+ returns one runnable ready for the next layer. Internally the router:
40
+
41
+ 1. builds/caches each raw ChatLiteLLM once;
42
+ 2. binds the same tools to primary and fallback models;
43
+ 3. composes the bound models with LangChain fallbacks;
44
+ 4. attaches usage tracking;
45
+ 5. caches the assembled runnable for the same model/toolset/context.
46
+
47
+ The provider remains hidden behind ChatLiteLLM/LiteLLM.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ config: AppConfig,
53
+ adapters: AdapterRegistry | None = None,
54
+ ) -> None:
55
+ self._config = config
56
+ self._adapters = adapters or AdapterRegistry()
57
+ self._model_cache: dict[str, ChatLiteLLM] = {} # type: ignore
58
+ self._runnable_cache: dict[tuple[str, tuple[str, ...], str | None], Runnable[Any, Any]] = {}
59
+ self._usage = UsageTracker()
60
+ self._lock = threading.RLock()
61
+
62
+ def get(
63
+ self,
64
+ model_name: str,
65
+ tools: Sequence[Any] | None = None,
66
+ *,
67
+ usage_context: str | None = None,
68
+ ) -> Runnable[Any, Any]:
69
+ """Return a configured, tool-bound, fallback-enabled runnable.
70
+
71
+ `tools` may be empty. When fallbacks exist, each fallback is bound to the
72
+ same tool set BEFORE the fallback wrapper is created.
73
+ """
74
+
75
+
76
+ self._model_cfg(model_name) # fail fast on unknown model name
77
+ tool_list = tuple(tools or ())
78
+ tool_key = tuple(self._tool_key(tool) for tool in tool_list)
79
+ cache_key = (model_name, tool_key, usage_context)
80
+
81
+ with self._lock:
82
+ cached = self._runnable_cache.get(cache_key)
83
+ if cached is not None:
84
+ return cached
85
+
86
+ runnable = self._assemble(
87
+ model_name,
88
+ tool_list,
89
+ usage_context=usage_context,
90
+ stack=(),
91
+ )
92
+
93
+ self._runnable_cache[cache_key] = runnable
94
+ return runnable
95
+
96
+ def capabilities(self, model_name: str) -> ModelCapabilities:
97
+ return self._model_cfg(model_name).capabilities
98
+
99
+ def usage(self) -> dict[str, UsageSnapshot]:
100
+ return self._usage.snapshot()
101
+
102
+ def _assemble(
103
+ self,
104
+ model_name: str,
105
+ tools: Sequence[Any],
106
+ *,
107
+ usage_context: str | None,
108
+ stack: tuple[str, ...],
109
+ ) -> Runnable[Any, Any]:
110
+ if model_name in stack:
111
+ cycle = " -> ".join((*stack, model_name))
112
+ raise RuntimeError(f"Fallback cycle detected while building: {cycle}")
113
+
114
+ cfg = self._model_cfg(model_name)
115
+
116
+ # 1. get raw ChatLiteLLM
117
+ primary = self._get_base_model(model_name)
118
+
119
+ # IMPORTANT: bind tools before wrapping in with_fallbacks().
120
+
121
+ # 2. Bind tools to primary
122
+ if tools:
123
+ if not cfg.capabilities.supports_tools:
124
+ raise ValueError(
125
+ f"Model '{model_name}' is configured with supports_tools=false "
126
+ "but tools were requested."
127
+ )
128
+ primary_bound = primary.bind_tools(list(tools))
129
+ else:
130
+ primary_bound = primary
131
+
132
+ # Attach usage tracking to each individual model branch. This is
133
+ # deliberately done BEFORE with_fallbacks(), so a fallback call is
134
+ # attributed to the fallback model rather than to the primary model.
135
+
136
+ # 3. Attach usage tracking to the primary
137
+ primary_runnable: Runnable[Any, Any] = primary_bound.with_config(
138
+ callbacks=[
139
+ _UsageCallback(self._usage, model_name, usage_context),
140
+ # DumpPayloadCallback(),
141
+ ],
142
+ tags=[f"llm-model:{model_name}"]
143
+ + ([f"usage:{usage_context}"] if usage_context else []),
144
+ )
145
+
146
+ # 4. Adapt this primary_runnable as per selected model quirks
147
+ adapter = self._adapters.get(cfg.adapter)
148
+
149
+ primary_runnable = adapter.adapt(
150
+ primary_runnable,
151
+ model_name=model_name,
152
+ config=cfg,
153
+ )
154
+
155
+ # 5. Build fallbacks recursively
156
+ fallbacks: list[Runnable[Any, Any]] = []
157
+ for fallback_name in cfg.fallbacks:
158
+ fallback = self._assemble(
159
+ fallback_name,
160
+ tools,
161
+ usage_context=usage_context,
162
+ stack=(*stack, model_name),
163
+ )
164
+ fallbacks.append(fallback)
165
+
166
+ # 6. Compose the already-prepared branches
167
+ if fallbacks:
168
+ return primary_runnable.with_fallbacks(fallbacks)
169
+
170
+ return primary_runnable
171
+
172
+ def _get_base_model(self, model_name: str) -> ChatLiteLLM: # type: ignore
173
+ with self._lock:
174
+ cached = self._model_cache.get(model_name)
175
+ if cached is not None:
176
+ return cached
177
+
178
+ model = self._build(model_name)
179
+ self._model_cache[model_name] = model
180
+ return model
181
+
182
+ def _model_cfg(self, model_name: str) -> ModelConfig:
183
+ try:
184
+ return self._config.models[model_name]
185
+ except KeyError:
186
+ raise KeyError(
187
+ f"Unknown model '{model_name}'. Defined models: {sorted(self._config.models)}"
188
+ ) from None
189
+
190
+ def _build(self, model_name: str) -> ChatLiteLLM: # type: ignore
191
+
192
+ from langchain_litellm import ChatLiteLLM
193
+
194
+ cfg = self._model_cfg(model_name)
195
+
196
+ direct = {k: v for k, v in cfg.params.items() if k in _DIRECT_PARAMS}
197
+ extra = {k: v for k, v in cfg.params.items() if k not in _DIRECT_PARAMS}
198
+ extra["stream_options"] = {"include_usage": True}
199
+
200
+ kwargs: dict[str, Any] = {
201
+ "model": cfg.model,
202
+ "streaming": True,
203
+ **direct,
204
+ }
205
+
206
+ api_key = get_api_key(cfg)
207
+ if api_key:
208
+ kwargs["api_key"] = api_key
209
+
210
+ if cfg.api_base:
211
+ kwargs["api_base"] = cfg.api_base
212
+
213
+ if extra:
214
+ kwargs["model_kwargs"] = extra
215
+
216
+ # log.info(
217
+ # "Building ChatLiteLLM '%s' -> %s",
218
+ # model_name,
219
+ # cfg.model,
220
+ # )
221
+
222
+ return ChatLiteLLM(**kwargs)
223
+
224
+
225
+ @staticmethod
226
+ def _tool_key(tool: Any) -> str:
227
+ # OpenAI-format dict
228
+ if isinstance(tool, dict):
229
+ fn = tool.get("function") or {}
230
+ name = fn.get("name") or tool.get("name")
231
+ if name:
232
+ return str(name)
233
+ return repr(tool)
234
+
235
+ # Tools object / BaseTool / callable
236
+ name = getattr(tool, "name", None)
237
+ if name:
238
+ return str(name)
239
+
240
+ qualname = getattr(tool, "__qualname__", None)
241
+ module = getattr(tool, "__module__", None)
242
+ if qualname:
243
+ return f"{module or ''}:{qualname}"
244
+
245
+ return repr(tool)
246
+
247
+
vamp_cli/llm/usage.py ADDED
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import threading
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from langchain_core.callbacks import BaseCallbackHandler
9
+ from langchain_core.outputs import LLMResult
10
+
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class UsageSnapshot:
17
+ calls: int = 0
18
+ input_tokens: int = 0
19
+ output_tokens: int = 0
20
+ total_tokens: int = 0
21
+ cost_usd: float = 0.0
22
+
23
+
24
+ class UsageTracker:
25
+ """Thread-safe in-process usage accumulator.
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ self._lock = threading.Lock()
30
+ self._totals: dict[str, UsageSnapshot] = {}
31
+
32
+ def record(
33
+ self,
34
+ model_name: str,
35
+ *,
36
+ input_tokens: int = 0,
37
+ output_tokens: int = 0,
38
+ total_tokens: int = 0,
39
+ cost_usd: float = 0.0,
40
+ ) -> None:
41
+ with self._lock:
42
+ previous = self._totals.get(model_name, UsageSnapshot())
43
+ self._totals[model_name] = UsageSnapshot(
44
+ calls=previous.calls + 1,
45
+ input_tokens=previous.input_tokens + input_tokens,
46
+ output_tokens=previous.output_tokens + output_tokens,
47
+ total_tokens=previous.total_tokens + total_tokens,
48
+ cost_usd=previous.cost_usd + cost_usd,
49
+ )
50
+
51
+ def snapshot(self) -> dict[str, UsageSnapshot]:
52
+ with self._lock:
53
+ return dict(self._totals)
54
+
55
+
56
+ class _UsageCallback(BaseCallbackHandler):
57
+ def __init__(
58
+ self,
59
+ tracker: UsageTracker,
60
+ model_name: str,
61
+ usage_context: str | None,
62
+ ) -> None:
63
+ self._tracker = tracker
64
+ self._model_name = model_name
65
+ self._usage_context = usage_context
66
+
67
+ def on_llm_end(self, response: LLMResult, **_: Any) -> None:
68
+ input_tokens, output_tokens, total_tokens, cost = _extract_usage(response)
69
+
70
+ key = (
71
+ f"{self._usage_context}::{self._model_name}"
72
+ if self._usage_context
73
+ else self._model_name
74
+ )
75
+
76
+ self._tracker.record(
77
+ key,
78
+ input_tokens=input_tokens,
79
+ output_tokens=output_tokens,
80
+ total_tokens=total_tokens,
81
+ cost_usd=cost,
82
+ )
83
+
84
+
85
+ def _extract_usage(response: LLMResult) -> tuple[int, int, int, float]:
86
+ """Best-effort extraction across common LangChain/LiteLLM response shapes."""
87
+
88
+ input_tokens = 0
89
+ output_tokens = 0
90
+ total_tokens = 0
91
+ cost = 0.0
92
+
93
+ llm_output = response.llm_output or {}
94
+ token_usage = (
95
+ llm_output.get("token_usage")
96
+ or llm_output.get("usage")
97
+ or llm_output.get("usage_metadata")
98
+ or {}
99
+ )
100
+
101
+ input_tokens = _as_int(
102
+ token_usage.get("input_tokens")
103
+ or token_usage.get("prompt_tokens")
104
+ or token_usage.get("inputTokenCount")
105
+ )
106
+ output_tokens = _as_int(
107
+ token_usage.get("output_tokens")
108
+ or token_usage.get("completion_tokens")
109
+ or token_usage.get("outputTokenCount")
110
+ )
111
+ total_tokens = _as_int(token_usage.get("total_tokens"))
112
+ cost = _as_float(
113
+ llm_output.get("response_cost")
114
+ or llm_output.get("cost")
115
+ or llm_output.get("total_cost")
116
+ )
117
+
118
+ # Some LangChain integrations place usage on the returned AIMessage.
119
+ for generation_group in response.generations:
120
+ for generation in generation_group:
121
+ message = getattr(generation, "message", None)
122
+ if message is None:
123
+ continue
124
+
125
+ usage = getattr(message, "usage_metadata", None) or {}
126
+ if usage:
127
+ input_tokens = input_tokens or _as_int(usage.get("input_tokens"))
128
+ output_tokens = output_tokens or _as_int(usage.get("output_tokens"))
129
+ total_tokens = total_tokens or _as_int(usage.get("total_tokens"))
130
+
131
+ metadata = getattr(message, "response_metadata", None) or {}
132
+ nested_usage = (
133
+ metadata.get("token_usage")
134
+ or metadata.get("usage")
135
+ or metadata.get("usage_metadata")
136
+ or {}
137
+ )
138
+ input_tokens = input_tokens or _as_int(
139
+ nested_usage.get("input_tokens")
140
+ or nested_usage.get("prompt_tokens")
141
+ )
142
+ output_tokens = output_tokens or _as_int(
143
+ nested_usage.get("output_tokens")
144
+ or nested_usage.get("completion_tokens")
145
+ )
146
+ total_tokens = total_tokens or _as_int(nested_usage.get("total_tokens"))
147
+ cost = cost or _as_float(
148
+ metadata.get("response_cost")
149
+ or metadata.get("cost")
150
+ or metadata.get("total_cost")
151
+ )
152
+
153
+ if total_tokens == 0:
154
+ total_tokens = input_tokens + output_tokens
155
+
156
+ return input_tokens, output_tokens, total_tokens, cost
157
+
158
+
159
+ def _as_int(value: Any) -> int:
160
+ try:
161
+ return int(value or 0)
162
+ except (TypeError, ValueError):
163
+ return 0
164
+
165
+
166
+ def _as_float(value: Any) -> float:
167
+ try:
168
+ return float(value or 0.0)
169
+ except (TypeError, ValueError):
170
+ return 0.0
@@ -0,0 +1,37 @@
1
+ import logging, sys
2
+ from pathlib import Path
3
+
4
+ TRACE = 5
5
+
6
+ def setup(level=logging.INFO, log_dir: str | None = None):
7
+ root = logging.getLogger()
8
+ for h in root.handlers[:]:
9
+ root.removeHandler(h)
10
+ root.setLevel(level)
11
+
12
+ fmt = logging.Formatter("%(asctime)s %(levelname)-7s %(name)s: %(message)s")
13
+
14
+ sh = logging.StreamHandler(sys.stderr)
15
+ sh.setFormatter(fmt)
16
+ root.addHandler(sh)
17
+
18
+ if not log_dir:
19
+ return
20
+
21
+ p = Path(log_dir)
22
+ p.mkdir(parents=True, exist_ok=True)
23
+
24
+ fh = logging.FileHandler(p / "vamp.log", mode="a")
25
+ fh.setFormatter(fmt)
26
+ root.addHandler(fh)
27
+
28
+ # if level <= logging.DEBUG:
29
+ # wire = logging.FileHandler(p / "wire.log", mode="w")
30
+ # wire.setLevel(TRACE)
31
+ # wire.setFormatter(fmt)
32
+ # for n in ("httpx", "httpcore", "litellm", "LiteLLM"):
33
+ # lg = logging.getLogger(n)
34
+ # lg.handlers.clear()
35
+ # lg.setLevel(TRACE)
36
+ # lg.addHandler(wire)
37
+ # lg.propagate = False