velune-cli 0.9.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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""Prompt format adaptation for different model families.
|
|
2
|
+
|
|
3
|
+
Adapts prompts and messages to match the native format of each model family,
|
|
4
|
+
improving output quality without requiring model calls. Each family has a
|
|
5
|
+
distinct prompt structure and system prompt handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from abc import ABC
|
|
12
|
+
from typing import Protocol
|
|
13
|
+
|
|
14
|
+
from velune.context.window import estimate_tokens
|
|
15
|
+
from velune.core.types.model import ModelDescriptor
|
|
16
|
+
from velune.models.family import ModelFamily, detect_family
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("velune.context.prompt_adaptation")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PromptTemplate(Protocol):
|
|
22
|
+
"""Protocol for prompt format templates.
|
|
23
|
+
|
|
24
|
+
Each model family has a distinct prompt structure.
|
|
25
|
+
Templates handle formatting messages and converting between formats.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
family: ModelFamily
|
|
29
|
+
|
|
30
|
+
# Guidance flags
|
|
31
|
+
prefer_shorter_system_prompts: bool
|
|
32
|
+
supports_xml_structured_output: bool
|
|
33
|
+
supports_json_mode: bool
|
|
34
|
+
max_recommended_system_tokens: int
|
|
35
|
+
|
|
36
|
+
def format_system(self, system_content: str) -> str:
|
|
37
|
+
"""Format a system prompt according to this family's convention."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def format_user(self, user_content: str) -> str:
|
|
41
|
+
"""Format a user message according to this family's convention."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def format_assistant(self, assistant_content: str) -> str:
|
|
45
|
+
"""Format an assistant message according to this family's convention."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def format_full_conversation(self, system: str, turns: list[dict[str, str]]) -> str:
|
|
49
|
+
"""Format a complete conversation into a single prompt string.
|
|
50
|
+
|
|
51
|
+
Used for models that expect a single text input rather than
|
|
52
|
+
message lists. Turns are dicts with 'role' and 'content' keys.
|
|
53
|
+
"""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
# Concrete Template Implementations
|
|
59
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class QwenTemplate(ABC):
|
|
63
|
+
"""Qwen models use ChatML format with <|im_start|>/<|im_end|> tokens."""
|
|
64
|
+
|
|
65
|
+
family = ModelFamily.QWEN
|
|
66
|
+
prefer_shorter_system_prompts = False
|
|
67
|
+
supports_xml_structured_output = True
|
|
68
|
+
supports_json_mode = True
|
|
69
|
+
max_recommended_system_tokens = 4096
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def format_system(system_content: str) -> str:
|
|
73
|
+
return f"<|im_start|>system\n{system_content}\n<|im_end|>"
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def format_user(user_content: str) -> str:
|
|
77
|
+
return f"<|im_start|>user\n{user_content}\n<|im_end|>"
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def format_assistant(assistant_content: str) -> str:
|
|
81
|
+
return f"<|im_start|>assistant\n{assistant_content}\n<|im_end|>"
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
|
|
85
|
+
parts = []
|
|
86
|
+
if system:
|
|
87
|
+
parts.append(QwenTemplate.format_system(system))
|
|
88
|
+
for turn in turns:
|
|
89
|
+
role = turn.get("role", "user")
|
|
90
|
+
content = turn.get("content", "")
|
|
91
|
+
if role == "user":
|
|
92
|
+
parts.append(QwenTemplate.format_user(content))
|
|
93
|
+
elif role == "assistant":
|
|
94
|
+
parts.append(QwenTemplate.format_assistant(content))
|
|
95
|
+
elif role == "system":
|
|
96
|
+
parts.append(QwenTemplate.format_system(content))
|
|
97
|
+
return "\n".join(parts) + "\n<|im_start|>assistant\n"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class DeepSeekTemplate(ABC):
|
|
101
|
+
"""DeepSeek models support reasoning with special thinking tokens."""
|
|
102
|
+
|
|
103
|
+
family = ModelFamily.DEEPSEEK
|
|
104
|
+
prefer_shorter_system_prompts = False
|
|
105
|
+
supports_xml_structured_output = True
|
|
106
|
+
supports_json_mode = True
|
|
107
|
+
max_recommended_system_tokens = 4096
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def format_system(system_content: str) -> str:
|
|
111
|
+
return f"System: {system_content}"
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def format_user(user_content: str) -> str:
|
|
115
|
+
return f"User: {user_content}"
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def format_assistant(assistant_content: str) -> str:
|
|
119
|
+
return f"Assistant: {assistant_content}"
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
|
|
123
|
+
parts = []
|
|
124
|
+
if system:
|
|
125
|
+
parts.append(DeepSeekTemplate.format_system(system))
|
|
126
|
+
for turn in turns:
|
|
127
|
+
role = turn.get("role", "user")
|
|
128
|
+
content = turn.get("content", "")
|
|
129
|
+
if role == "user":
|
|
130
|
+
parts.append(DeepSeekTemplate.format_user(content))
|
|
131
|
+
elif role == "assistant":
|
|
132
|
+
parts.append(DeepSeekTemplate.format_assistant(content))
|
|
133
|
+
elif role == "system":
|
|
134
|
+
parts.append(DeepSeekTemplate.format_system(content))
|
|
135
|
+
return "\n\n".join(parts) + "\n\nAssistant: "
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Llama3Template(ABC):
|
|
139
|
+
"""Llama3 uses [INST] / <<SYS>> markers for instruction and system prompts."""
|
|
140
|
+
|
|
141
|
+
family = ModelFamily.LLAMA3
|
|
142
|
+
prefer_shorter_system_prompts = False
|
|
143
|
+
supports_xml_structured_output = True
|
|
144
|
+
supports_json_mode = False
|
|
145
|
+
max_recommended_system_tokens = 2048
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def format_system(system_content: str) -> str:
|
|
149
|
+
return f"<<SYS>>\n{system_content}\n<</SYS>>"
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def format_user(user_content: str) -> str:
|
|
153
|
+
return f"[INST] {user_content} [/INST]"
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def format_assistant(assistant_content: str) -> str:
|
|
157
|
+
return assistant_content
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
|
|
161
|
+
parts = []
|
|
162
|
+
system_marker = f"\n{Llama3Template.format_system(system)}\n" if system else ""
|
|
163
|
+
|
|
164
|
+
for i, turn in enumerate(turns):
|
|
165
|
+
role = turn.get("role", "user")
|
|
166
|
+
content = turn.get("content", "")
|
|
167
|
+
|
|
168
|
+
if role == "user":
|
|
169
|
+
if i == 0 and system:
|
|
170
|
+
# First user turn gets the system prompt embedded
|
|
171
|
+
parts.append(f"[INST] {system_marker}{content} [/INST]")
|
|
172
|
+
else:
|
|
173
|
+
parts.append(f"[INST] {content} [/INST]")
|
|
174
|
+
elif role == "assistant":
|
|
175
|
+
parts.append(f" {content} ")
|
|
176
|
+
|
|
177
|
+
return "".join(parts)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class PhiTemplate(ABC):
|
|
181
|
+
"""Phi models respond better to shorter, more direct prompts."""
|
|
182
|
+
|
|
183
|
+
family = ModelFamily.PHI
|
|
184
|
+
prefer_shorter_system_prompts = True
|
|
185
|
+
supports_xml_structured_output = False
|
|
186
|
+
supports_json_mode = False
|
|
187
|
+
max_recommended_system_tokens = 512
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def format_system(system_content: str) -> str:
|
|
191
|
+
return system_content
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def format_user(user_content: str) -> str:
|
|
195
|
+
return user_content
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def format_assistant(assistant_content: str) -> str:
|
|
199
|
+
return assistant_content
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
|
|
203
|
+
parts = []
|
|
204
|
+
if system:
|
|
205
|
+
parts.append(system)
|
|
206
|
+
for turn in turns:
|
|
207
|
+
content = turn.get("content", "")
|
|
208
|
+
if content:
|
|
209
|
+
parts.append(content)
|
|
210
|
+
return "\n\n".join(parts)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class MistralTemplate(ABC):
|
|
214
|
+
"""Mistral uses [INST] markers but places system prompt differently."""
|
|
215
|
+
|
|
216
|
+
family = ModelFamily.MISTRAL
|
|
217
|
+
prefer_shorter_system_prompts = False
|
|
218
|
+
supports_xml_structured_output = False
|
|
219
|
+
supports_json_mode = False
|
|
220
|
+
max_recommended_system_tokens = 2048
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def format_system(system_content: str) -> str:
|
|
224
|
+
return f"[SYSTEM] {system_content}"
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def format_user(user_content: str) -> str:
|
|
228
|
+
return f"[INST] {user_content} [/INST]"
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def format_assistant(assistant_content: str) -> str:
|
|
232
|
+
return assistant_content
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
|
|
236
|
+
parts = []
|
|
237
|
+
if system:
|
|
238
|
+
parts.append(MistralTemplate.format_system(system))
|
|
239
|
+
|
|
240
|
+
for turn in turns:
|
|
241
|
+
role = turn.get("role", "user")
|
|
242
|
+
content = turn.get("content", "")
|
|
243
|
+
|
|
244
|
+
if role == "user":
|
|
245
|
+
parts.append(f"[INST] {content} [/INST]")
|
|
246
|
+
elif role == "assistant":
|
|
247
|
+
parts.append(f" {content}")
|
|
248
|
+
|
|
249
|
+
return " ".join(parts)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class GemmaTemplate(ABC):
|
|
253
|
+
"""Gemma is similar to Llama3 with a direct message format."""
|
|
254
|
+
|
|
255
|
+
family = ModelFamily.GEMMA
|
|
256
|
+
prefer_shorter_system_prompts = False
|
|
257
|
+
supports_xml_structured_output = False
|
|
258
|
+
supports_json_mode = False
|
|
259
|
+
max_recommended_system_tokens = 2048
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def format_system(system_content: str) -> str:
|
|
263
|
+
return system_content
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def format_user(user_content: str) -> str:
|
|
267
|
+
return f"<start_of_turn>user\n{user_content}<end_of_turn>"
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def format_assistant(assistant_content: str) -> str:
|
|
271
|
+
return f"<start_of_turn>model\n{assistant_content}<end_of_turn>"
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
|
|
275
|
+
parts = []
|
|
276
|
+
if system:
|
|
277
|
+
parts.append(f"<start_of_turn>user\n{system}<end_of_turn>")
|
|
278
|
+
|
|
279
|
+
for turn in turns:
|
|
280
|
+
role = turn.get("role", "user")
|
|
281
|
+
content = turn.get("content", "")
|
|
282
|
+
|
|
283
|
+
if role == "user":
|
|
284
|
+
parts.append(f"<start_of_turn>user\n{content}<end_of_turn>")
|
|
285
|
+
elif role == "assistant":
|
|
286
|
+
parts.append(f"<start_of_turn>model\n{content}<end_of_turn>")
|
|
287
|
+
|
|
288
|
+
parts.append("<start_of_turn>model\n")
|
|
289
|
+
return "".join(parts)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class StandardAPITemplate(ABC):
|
|
293
|
+
"""Standard API format for Claude, GPT, Gemini, etc.
|
|
294
|
+
|
|
295
|
+
Uses message lists with role/content pairs.
|
|
296
|
+
System prompt is a separate message with role="system".
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
family = ModelFamily.UNKNOWN
|
|
300
|
+
prefer_shorter_system_prompts = False
|
|
301
|
+
supports_xml_structured_output = True
|
|
302
|
+
supports_json_mode = True
|
|
303
|
+
max_recommended_system_tokens = 4096
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def format_system(system_content: str) -> str:
|
|
307
|
+
return system_content
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def format_user(user_content: str) -> str:
|
|
311
|
+
return user_content
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def format_assistant(assistant_content: str) -> str:
|
|
315
|
+
return assistant_content
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def format_full_conversation(system: str, turns: list[dict[str, str]]) -> str:
|
|
319
|
+
# For standard API, this just reconstructs the message list as text
|
|
320
|
+
# (mainly for debugging or conversion)
|
|
321
|
+
parts = []
|
|
322
|
+
if system:
|
|
323
|
+
parts.append(f"System: {system}")
|
|
324
|
+
for turn in turns:
|
|
325
|
+
role = turn.get("role", "unknown")
|
|
326
|
+
content = turn.get("content", "")
|
|
327
|
+
parts.append(f"{role.capitalize()}: {content}")
|
|
328
|
+
return "\n\n".join(parts)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
332
|
+
# Prompt Adaptation Engine
|
|
333
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class PromptAdaptationEngine:
|
|
337
|
+
"""Adapts prompts and messages to match each model family's format.
|
|
338
|
+
|
|
339
|
+
Handles prompt reformatting, system prompt truncation, and message
|
|
340
|
+
list conversion for different model families.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
def __init__(self) -> None:
|
|
344
|
+
"""Initialize with template implementations for each family."""
|
|
345
|
+
self._templates = {
|
|
346
|
+
ModelFamily.QWEN: QwenTemplate,
|
|
347
|
+
ModelFamily.DEEPSEEK: DeepSeekTemplate,
|
|
348
|
+
ModelFamily.LLAMA3: Llama3Template,
|
|
349
|
+
ModelFamily.PHI: PhiTemplate,
|
|
350
|
+
ModelFamily.MISTRAL: MistralTemplate,
|
|
351
|
+
ModelFamily.GEMMA: GemmaTemplate,
|
|
352
|
+
ModelFamily.CLAUDE: StandardAPITemplate,
|
|
353
|
+
ModelFamily.GPT: StandardAPITemplate,
|
|
354
|
+
ModelFamily.GEMINI: StandardAPITemplate,
|
|
355
|
+
ModelFamily.UNKNOWN: StandardAPITemplate,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
def get_template(self, model: ModelDescriptor) -> type:
|
|
359
|
+
"""Get the prompt template for a model.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
model:
|
|
364
|
+
The model descriptor.
|
|
365
|
+
|
|
366
|
+
Returns
|
|
367
|
+
-------
|
|
368
|
+
type:
|
|
369
|
+
The template class for the model's family.
|
|
370
|
+
"""
|
|
371
|
+
family = detect_family(model.model_id)
|
|
372
|
+
return self._templates.get(family, StandardAPITemplate)
|
|
373
|
+
|
|
374
|
+
def adapt_system_prompt(self, system: str, model: ModelDescriptor) -> str:
|
|
375
|
+
"""Adapt and truncate a system prompt to match the model's preferences.
|
|
376
|
+
|
|
377
|
+
Parameters
|
|
378
|
+
----------
|
|
379
|
+
system:
|
|
380
|
+
The original system prompt.
|
|
381
|
+
model:
|
|
382
|
+
The target model descriptor.
|
|
383
|
+
|
|
384
|
+
Returns
|
|
385
|
+
-------
|
|
386
|
+
str:
|
|
387
|
+
The adapted system prompt, truncated if needed.
|
|
388
|
+
"""
|
|
389
|
+
if not system:
|
|
390
|
+
return ""
|
|
391
|
+
|
|
392
|
+
template = self.get_template(model)
|
|
393
|
+
max_tokens = template.max_recommended_system_tokens
|
|
394
|
+
|
|
395
|
+
# Check if truncation is needed
|
|
396
|
+
token_count = estimate_tokens(system)
|
|
397
|
+
if token_count > max_tokens:
|
|
398
|
+
# Truncate to fit within budget
|
|
399
|
+
target_char_count = max_tokens * 4 # Rough estimate
|
|
400
|
+
truncated = system[:target_char_count].rsplit(" ", 1)[0]
|
|
401
|
+
truncated += "\n[... truncated due to model constraints ...]"
|
|
402
|
+
logger.debug(
|
|
403
|
+
"System prompt truncated from %d to %d tokens for %s",
|
|
404
|
+
token_count,
|
|
405
|
+
estimate_tokens(truncated),
|
|
406
|
+
model.model_id,
|
|
407
|
+
)
|
|
408
|
+
return truncated
|
|
409
|
+
|
|
410
|
+
return system
|
|
411
|
+
|
|
412
|
+
def adapt_messages(
|
|
413
|
+
self, messages: list[dict[str, str]], model: ModelDescriptor
|
|
414
|
+
) -> list[dict[str, str]]:
|
|
415
|
+
"""Adapt message list to match the model's expected format.
|
|
416
|
+
|
|
417
|
+
For models with specialized formats (Qwen, DeepSeek, Llama3, etc.),
|
|
418
|
+
this converts the standard message list into the model's native format.
|
|
419
|
+
|
|
420
|
+
For cloud APIs (Claude, GPT, Gemini), the message list is returned
|
|
421
|
+
unchanged since they already use the standard format.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
messages:
|
|
426
|
+
Standard message list with 'role' and 'content' keys.
|
|
427
|
+
model:
|
|
428
|
+
The target model descriptor.
|
|
429
|
+
|
|
430
|
+
Returns
|
|
431
|
+
-------
|
|
432
|
+
list[dict[str, str]]:
|
|
433
|
+
Messages in the format expected by the model.
|
|
434
|
+
"""
|
|
435
|
+
family = detect_family(model.model_id)
|
|
436
|
+
|
|
437
|
+
# Cloud APIs and unknown models use standard message format; return as-is
|
|
438
|
+
if family in (ModelFamily.CLAUDE, ModelFamily.GPT, ModelFamily.GEMINI, ModelFamily.UNKNOWN):
|
|
439
|
+
return messages
|
|
440
|
+
|
|
441
|
+
# For specialized formats, wrap in a single "text" message
|
|
442
|
+
# (Some local models expect a single text prompt)
|
|
443
|
+
template = self._templates[family]
|
|
444
|
+
system = ""
|
|
445
|
+
turns = []
|
|
446
|
+
|
|
447
|
+
for msg in messages:
|
|
448
|
+
role = msg.get("role", "user")
|
|
449
|
+
content = msg.get("content", "")
|
|
450
|
+
|
|
451
|
+
if role == "system":
|
|
452
|
+
system = content
|
|
453
|
+
else:
|
|
454
|
+
turns.append({"role": role, "content": content})
|
|
455
|
+
|
|
456
|
+
# Format as complete conversation text
|
|
457
|
+
formatted_text = template.format_full_conversation(system, turns)
|
|
458
|
+
|
|
459
|
+
return [{"role": "user", "content": formatted_text}]
|
|
460
|
+
|
|
461
|
+
def get_capabilities(self, model: ModelDescriptor) -> dict[str, bool | int]:
|
|
462
|
+
"""Get formatting capabilities for a model.
|
|
463
|
+
|
|
464
|
+
Parameters
|
|
465
|
+
----------
|
|
466
|
+
model:
|
|
467
|
+
The model descriptor.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
dict:
|
|
472
|
+
Capabilities including XML support, JSON mode, max system tokens.
|
|
473
|
+
"""
|
|
474
|
+
template = self.get_template(model)
|
|
475
|
+
return {
|
|
476
|
+
"prefer_shorter_prompts": template.prefer_shorter_system_prompts,
|
|
477
|
+
"supports_xml": template.supports_xml_structured_output,
|
|
478
|
+
"supports_json_mode": template.supports_json_mode,
|
|
479
|
+
"max_system_tokens": template.max_recommended_system_tokens,
|
|
480
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Context section definitions and chunk metadata.
|
|
2
|
+
|
|
3
|
+
Defines the canonical sections that comprise the context window,
|
|
4
|
+
with explicit ordering and priority handling for trimming.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import IntEnum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContextSection(IntEnum):
|
|
14
|
+
"""Canonical sections in the context window, ordered by priority.
|
|
15
|
+
|
|
16
|
+
Lower numbers = higher priority in the final assembled context.
|
|
17
|
+
Never trimmed: SYSTEM_PROMPT (always first), ARCHITECTURAL_DRIFT (urgent),
|
|
18
|
+
CURRENT_PROMPT (always last).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
SYSTEM_PROMPT = 1 # System instructions and role definition
|
|
22
|
+
COGNITIVE_CONTINUITY = 2 # Lineage decisions, failed experiments, lessons learned
|
|
23
|
+
ARCHITECTURAL_DRIFT = 3 # URGENT: Current violations, breaking changes
|
|
24
|
+
REPOSITORY_SNAPSHOT = 4 # File structure, detected architecture, codebase state
|
|
25
|
+
RETRIEVED_CONTEXT = 5 # Results from RetrievalPipeline (most droppable)
|
|
26
|
+
WORKING_MEMORY = 6 # Current session turns, conversation history
|
|
27
|
+
CURRENT_PROMPT = 7 # The immediate user request (always last)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ContextChunk:
|
|
32
|
+
"""A chunk of assembled context with metadata for budget management.
|
|
33
|
+
|
|
34
|
+
Chunks are grouped by section and trimmed based on trust_score and priority
|
|
35
|
+
when the assembled context exceeds budget limits.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
section: ContextSection
|
|
39
|
+
content: str
|
|
40
|
+
token_count: int
|
|
41
|
+
source: str # Debugging: where did this chunk come from?
|
|
42
|
+
trust_score: float = 1.0 # 0.0 to 1.0 (lower = more likely to trim)
|
|
43
|
+
priority: float = 0.5 # Within-section priority for trimming
|
|
44
|
+
metadata: dict = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
"""Validate chunk integrity."""
|
|
48
|
+
if not (0.0 <= self.trust_score <= 1.0):
|
|
49
|
+
raise ValueError(f"trust_score must be 0.0-1.0, got {self.trust_score}")
|
|
50
|
+
if not self.content or not isinstance(self.content, str):
|
|
51
|
+
raise ValueError("content must be a non-empty string")
|
|
52
|
+
if self.token_count < 0:
|
|
53
|
+
raise ValueError(f"token_count must be non-negative, got {self.token_count}")
|
|
54
|
+
|
|
55
|
+
def is_trimmable(self) -> bool:
|
|
56
|
+
"""Check if this chunk can be trimmed when over budget.
|
|
57
|
+
|
|
58
|
+
NEVER trim: SYSTEM_PROMPT, ARCHITECTURAL_DRIFT, CURRENT_PROMPT.
|
|
59
|
+
"""
|
|
60
|
+
return self.section not in (
|
|
61
|
+
ContextSection.SYSTEM_PROMPT,
|
|
62
|
+
ContextSection.ARCHITECTURAL_DRIFT,
|
|
63
|
+
ContextSection.CURRENT_PROMPT,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def trim_score(self) -> float:
|
|
67
|
+
"""Score for sorting chunks by trim-ability (lower = trim first).
|
|
68
|
+
|
|
69
|
+
Lower scores are candidates for trimming. Combines trust_score
|
|
70
|
+
(lower = less trustworthy) and priority (lower = less important).
|
|
71
|
+
"""
|
|
72
|
+
return self.trust_score * self.priority
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class ContextAssemblyReport:
|
|
77
|
+
"""Report of how context was assembled and trimmed."""
|
|
78
|
+
|
|
79
|
+
total_chunks_received: int
|
|
80
|
+
total_tokens_requested: int
|
|
81
|
+
total_tokens_assembled: int
|
|
82
|
+
sections_present: list[ContextSection] = field(default_factory=list)
|
|
83
|
+
sections_trimmed: dict[ContextSection, int] = field(default_factory=dict)
|
|
84
|
+
chunks_dropped: int = 0
|
|
85
|
+
budget_exceeded: bool = False
|
|
86
|
+
|
|
87
|
+
def __str__(self) -> str:
|
|
88
|
+
"""Human-readable assembly report."""
|
|
89
|
+
lines = [
|
|
90
|
+
"ContextAssemblyReport:",
|
|
91
|
+
f" Chunks: {self.total_chunks_received} received, {self.chunks_dropped} dropped",
|
|
92
|
+
f" Tokens: {self.total_tokens_assembled}/{self.total_tokens_requested}",
|
|
93
|
+
f" Sections: {len(self.sections_present)} present",
|
|
94
|
+
]
|
|
95
|
+
if self.sections_trimmed:
|
|
96
|
+
lines.append(f" Trimmed: {self.sections_trimmed}")
|
|
97
|
+
if self.budget_exceeded:
|
|
98
|
+
lines.append(" WARNING: Budget exceeded even after trimming")
|
|
99
|
+
return "\n".join(lines)
|