delimit-cli 3.9.1 → 3.9.3
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.
- package/bin/delimit-setup.js +6 -4
- package/gateway/ai/deliberation.py +443 -13
- package/gateway/ai/governance.py +399 -7
- package/gateway/ai/license.py +44 -129
- package/gateway/ai/license_core.py +196 -0
- package/gateway/ai/server.py +43 -6
- package/package.json +2 -1
package/bin/delimit-setup.js
CHANGED
|
@@ -104,7 +104,8 @@ async function main() {
|
|
|
104
104
|
const arch = (() => { try { return execSync('uname -m', { encoding: 'utf-8' }).trim(); } catch { return 'x86_64'; } })();
|
|
105
105
|
const osName = process.platform === 'darwin' ? 'macos' : 'linux';
|
|
106
106
|
const artifact = `${osName}-${arch}-${pyVer}`;
|
|
107
|
-
const
|
|
107
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
108
|
+
const proVersion = pkg.proModuleVersion || '3.8.2';
|
|
108
109
|
const proUrl = `https://delimit.ai/releases/v${proVersion}/delimit-pro-${artifact}.tar.gz`;
|
|
109
110
|
|
|
110
111
|
try {
|
|
@@ -430,9 +431,10 @@ function getClaudeMdContent() {
|
|
|
430
431
|
One workspace for every AI coding assistant.
|
|
431
432
|
|
|
432
433
|
## On every session start:
|
|
433
|
-
1. Call \`
|
|
434
|
-
2.
|
|
435
|
-
3. If
|
|
434
|
+
1. Call \`delimit_gov_health\` to show governance status
|
|
435
|
+
2. Call \`delimit_ledger_context\` to check for open tasks
|
|
436
|
+
3. If returning user, summarize what's pending
|
|
437
|
+
4. If new project, call \`delimit_scan\` to discover what Delimit can do
|
|
436
438
|
|
|
437
439
|
## Try these:
|
|
438
440
|
- "scan this project" -- discover what Delimit can do here
|
|
@@ -1,20 +1,450 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Delimit Deliberation Engine — Multi-model
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
Delimit Deliberation Engine — Multi-round consensus with real model-to-model debate.
|
|
3
|
+
|
|
4
|
+
Passes each model's EXACT raw response to the other models for counter-arguments.
|
|
5
|
+
Rounds continue until unanimous agreement or max rounds reached.
|
|
6
|
+
|
|
7
|
+
Models are configured via ~/.delimit/models.json — users choose which AI models
|
|
8
|
+
to include in deliberations. Supports any OpenAI-compatible API.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import time
|
|
16
|
+
import urllib.request
|
|
17
|
+
import urllib.error
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("delimit.deliberation")
|
|
22
|
+
|
|
23
|
+
DELIBERATION_DIR = Path.home() / ".delimit" / "deliberations"
|
|
24
|
+
MODELS_CONFIG = Path.home() / ".delimit" / "models.json"
|
|
25
|
+
|
|
26
|
+
DEFAULT_MODELS = {
|
|
27
|
+
"grok": {
|
|
28
|
+
"name": "Grok",
|
|
29
|
+
"api_url": "https://api.x.ai/v1/chat/completions",
|
|
30
|
+
"model": "grok-4-0709",
|
|
31
|
+
"env_key": "XAI_API_KEY",
|
|
32
|
+
"enabled": False,
|
|
33
|
+
},
|
|
34
|
+
"gemini": {
|
|
35
|
+
"name": "Gemini",
|
|
36
|
+
"api_url": "https://us-central1-aiplatform.googleapis.com/v1/projects/{project}/locations/us-central1/publishers/google/models/gemini-2.5-flash:generateContent",
|
|
37
|
+
"model": "gemini-2.5-flash",
|
|
38
|
+
"env_key": "GOOGLE_APPLICATION_CREDENTIALS",
|
|
39
|
+
"enabled": False,
|
|
40
|
+
"format": "vertex_ai",
|
|
41
|
+
"prefer_cli": True, # Use gemini CLI if available (Ultra plan), fall back to Vertex AI
|
|
42
|
+
"cli_command": "gemini",
|
|
43
|
+
},
|
|
44
|
+
"openai": {
|
|
45
|
+
"name": "OpenAI",
|
|
46
|
+
"api_url": "https://api.openai.com/v1/chat/completions",
|
|
47
|
+
"model": "gpt-4o",
|
|
48
|
+
"env_key": "OPENAI_API_KEY",
|
|
49
|
+
"enabled": False,
|
|
50
|
+
"prefer_cli": True, # Use Codex CLI if available, fall back to API
|
|
51
|
+
},
|
|
52
|
+
"anthropic": {
|
|
53
|
+
"name": "Claude",
|
|
54
|
+
"api_url": "https://api.anthropic.com/v1/messages",
|
|
55
|
+
"model": "claude-sonnet-4-5-20250514",
|
|
56
|
+
"env_key": "ANTHROPIC_API_KEY",
|
|
57
|
+
"enabled": False,
|
|
58
|
+
"format": "anthropic",
|
|
59
|
+
"prefer_cli": True, # Use claude CLI if available (Pro/Max), fall back to API
|
|
60
|
+
"cli_command": "claude",
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_models_config() -> Dict[str, Any]:
|
|
66
|
+
"""Load model configuration. Auto-detects available API keys."""
|
|
67
|
+
if MODELS_CONFIG.exists():
|
|
68
|
+
try:
|
|
69
|
+
return json.loads(MODELS_CONFIG.read_text())
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# Auto-detect from environment
|
|
74
|
+
config = {}
|
|
75
|
+
for model_id, defaults in DEFAULT_MODELS.items():
|
|
76
|
+
key = os.environ.get(defaults.get("env_key", ""), "")
|
|
77
|
+
|
|
78
|
+
if defaults.get("prefer_cli"):
|
|
79
|
+
# Prefer CLI (uses existing subscription) over API (extra cost)
|
|
80
|
+
import shutil
|
|
81
|
+
cli_cmd = defaults.get("cli_command", "codex")
|
|
82
|
+
cli_path = shutil.which(cli_cmd)
|
|
83
|
+
if cli_path:
|
|
84
|
+
config[model_id] = {
|
|
85
|
+
**defaults,
|
|
86
|
+
"format": "codex_cli",
|
|
87
|
+
"enabled": True,
|
|
88
|
+
"codex_path": cli_path,
|
|
89
|
+
"backend": "cli",
|
|
90
|
+
}
|
|
91
|
+
elif key:
|
|
92
|
+
config[model_id] = {
|
|
93
|
+
**defaults,
|
|
94
|
+
"api_key": key,
|
|
95
|
+
"enabled": True,
|
|
96
|
+
"backend": "api",
|
|
97
|
+
}
|
|
98
|
+
else:
|
|
99
|
+
config[model_id] = {**defaults, "enabled": False}
|
|
100
|
+
else:
|
|
101
|
+
config[model_id] = {
|
|
102
|
+
**defaults,
|
|
103
|
+
"api_key": key,
|
|
104
|
+
"enabled": bool(key),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return config
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def configure_models() -> Dict[str, Any]:
|
|
111
|
+
"""Return current model configuration and what's available."""
|
|
112
|
+
config = get_models_config()
|
|
113
|
+
available = {k: v for k, v in config.items() if v.get("enabled")}
|
|
114
|
+
missing = {k: v for k, v in config.items() if not v.get("enabled")}
|
|
115
|
+
|
|
116
|
+
model_details = {}
|
|
117
|
+
for k, v in available.items():
|
|
118
|
+
backend = v.get("backend", "api")
|
|
119
|
+
if v.get("format") == "codex_cli":
|
|
120
|
+
backend = "cli"
|
|
121
|
+
model_details[k] = {"name": v.get("name", k), "backend": backend, "model": v.get("model", "")}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"configured_models": list(available.keys()),
|
|
125
|
+
"model_details": model_details,
|
|
126
|
+
"missing_models": {k: f"Set {v.get('env_key', 'key')} or install {v.get('cli_command', '')} CLI" for k, v in missing.items()},
|
|
127
|
+
"config_path": str(MODELS_CONFIG),
|
|
128
|
+
"note": "CLI backends use your existing subscription (no extra API cost). "
|
|
129
|
+
"API backends require separate API keys.",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _call_cli(prompt: str, system_prompt: str = "", cli_path: str = "", cli_command: str = "codex") -> str:
|
|
134
|
+
"""Call an AI CLI tool (codex or claude) via subprocess. Uses existing subscription — no API cost."""
|
|
135
|
+
import subprocess
|
|
136
|
+
|
|
137
|
+
if not cli_path:
|
|
138
|
+
cli_path = shutil.which(cli_command) or ""
|
|
139
|
+
if not cli_path:
|
|
140
|
+
return f"[{cli_command} unavailable — CLI not found in PATH]"
|
|
141
|
+
|
|
142
|
+
full_prompt = f"{system_prompt}\n\n{prompt}" if system_prompt else prompt
|
|
143
|
+
|
|
144
|
+
# Build command based on which CLI
|
|
145
|
+
if "claude" in cli_command:
|
|
146
|
+
cmd = [cli_path, "--print", "--dangerously-skip-permissions", full_prompt]
|
|
147
|
+
else:
|
|
148
|
+
# codex
|
|
149
|
+
cmd = [cli_path, "exec", "--dangerously-bypass-approvals-and-sandbox", full_prompt]
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
153
|
+
output = result.stdout.strip()
|
|
154
|
+
if not output and result.stderr:
|
|
155
|
+
return f"[{cli_command} error: {result.stderr[:300]}]"
|
|
156
|
+
return output or f"[{cli_command} returned empty response]"
|
|
157
|
+
except subprocess.TimeoutExpired:
|
|
158
|
+
return f"[{cli_command} timed out after 120s]"
|
|
159
|
+
except Exception as e:
|
|
160
|
+
return f"[{cli_command} error: {e}]"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _call_model(model_id: str, config: Dict, prompt: str, system_prompt: str = "") -> str:
|
|
164
|
+
"""Call any supported model — OpenAI-compatible API, Vertex AI, or CLI (codex/claude)."""
|
|
165
|
+
fmt = config.get("format", "openai")
|
|
166
|
+
|
|
167
|
+
# CLI-based models (codex, claude) — uses existing subscription, no API cost
|
|
168
|
+
if fmt == "codex_cli":
|
|
169
|
+
cli_path = config.get("codex_path", "")
|
|
170
|
+
cli_command = config.get("cli_command", "codex")
|
|
171
|
+
return _call_cli(prompt, system_prompt, cli_path=cli_path, cli_command=cli_command)
|
|
172
|
+
|
|
173
|
+
api_key = config.get("api_key") or os.environ.get(config.get("env_key", ""), "")
|
|
174
|
+
# Vertex AI uses service account auth, not API key
|
|
175
|
+
if not api_key and fmt != "vertex_ai":
|
|
176
|
+
return f"[{config.get('name', model_id)} unavailable — {config.get('env_key')} not set]"
|
|
177
|
+
|
|
178
|
+
api_url = config["api_url"]
|
|
179
|
+
model = config.get("model", "")
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
if fmt == "vertex_ai":
|
|
183
|
+
# Vertex AI format — use google-auth for access token
|
|
184
|
+
try:
|
|
185
|
+
import google.auth
|
|
186
|
+
import google.auth.transport.requests
|
|
187
|
+
# Explicitly set credentials path if not in env
|
|
188
|
+
creds_path = "/root/.config/gcloud/application_default_credentials.json"
|
|
189
|
+
if not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") and os.path.exists(creds_path):
|
|
190
|
+
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = creds_path
|
|
191
|
+
creds, project = google.auth.default()
|
|
192
|
+
creds.refresh(google.auth.transport.requests.Request())
|
|
193
|
+
actual_url = api_url.replace("{project}", project or os.environ.get("GOOGLE_CLOUD_PROJECT", ""))
|
|
194
|
+
data = json.dumps({
|
|
195
|
+
"contents": [{"role": "user", "parts": [{"text": f"{system_prompt}\n\n{prompt}" if system_prompt else prompt}]}],
|
|
196
|
+
"generationConfig": {"maxOutputTokens": 4096, "temperature": 0.7},
|
|
197
|
+
}).encode()
|
|
198
|
+
req = urllib.request.Request(
|
|
199
|
+
actual_url,
|
|
200
|
+
data=data,
|
|
201
|
+
headers={
|
|
202
|
+
"Authorization": f"Bearer {creds.token}",
|
|
203
|
+
"Content-Type": "application/json",
|
|
204
|
+
},
|
|
205
|
+
method="POST",
|
|
206
|
+
)
|
|
207
|
+
except ImportError:
|
|
208
|
+
return f"[Gemini unavailable — install google-auth: pip install google-auth]"
|
|
209
|
+
elif fmt == "google":
|
|
210
|
+
# Google Generative AI format (API key)
|
|
211
|
+
data = json.dumps({
|
|
212
|
+
"contents": [{"role": "user", "parts": [{"text": f"{system_prompt}\n\n{prompt}" if system_prompt else prompt}]}],
|
|
213
|
+
"generationConfig": {"maxOutputTokens": 4096, "temperature": 0.7},
|
|
214
|
+
}).encode()
|
|
215
|
+
req = urllib.request.Request(
|
|
216
|
+
f"{api_url}?key={api_key}",
|
|
217
|
+
data=data,
|
|
218
|
+
headers={"Content-Type": "application/json"},
|
|
219
|
+
method="POST",
|
|
220
|
+
)
|
|
221
|
+
elif fmt == "anthropic":
|
|
222
|
+
# Anthropic Messages API
|
|
223
|
+
data = json.dumps({
|
|
224
|
+
"model": model,
|
|
225
|
+
"max_tokens": 4096,
|
|
226
|
+
"system": system_prompt or "You are a helpful assistant participating in a multi-model deliberation.",
|
|
227
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
228
|
+
}).encode()
|
|
229
|
+
req = urllib.request.Request(
|
|
230
|
+
api_url,
|
|
231
|
+
data=data,
|
|
232
|
+
headers={
|
|
233
|
+
"x-api-key": api_key,
|
|
234
|
+
"anthropic-version": "2023-06-01",
|
|
235
|
+
"Content-Type": "application/json",
|
|
236
|
+
"User-Agent": "Delimit/3.6.0",
|
|
237
|
+
},
|
|
238
|
+
method="POST",
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
# OpenAI-compatible format (works for xAI, OpenAI, etc.)
|
|
242
|
+
messages = []
|
|
243
|
+
if system_prompt:
|
|
244
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
245
|
+
messages.append({"role": "user", "content": prompt})
|
|
246
|
+
|
|
247
|
+
data = json.dumps({
|
|
248
|
+
"model": model,
|
|
249
|
+
"messages": messages,
|
|
250
|
+
"temperature": 0.7,
|
|
251
|
+
"max_tokens": 4096,
|
|
252
|
+
}).encode()
|
|
253
|
+
req = urllib.request.Request(
|
|
254
|
+
api_url,
|
|
255
|
+
data=data,
|
|
256
|
+
headers={
|
|
257
|
+
"Authorization": f"Bearer {api_key}",
|
|
258
|
+
"Content-Type": "application/json",
|
|
259
|
+
"User-Agent": "Delimit/3.6.0",
|
|
260
|
+
},
|
|
261
|
+
method="POST",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
265
|
+
result = json.loads(resp.read())
|
|
266
|
+
|
|
267
|
+
if fmt in ("google", "vertex_ai"):
|
|
268
|
+
return result["candidates"][0]["content"]["parts"][0]["text"]
|
|
269
|
+
elif fmt == "anthropic":
|
|
270
|
+
return result["content"][0]["text"]
|
|
271
|
+
else:
|
|
272
|
+
return result["choices"][0]["message"]["content"]
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return f"[{config.get('name', model_id)} error: {e}]"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def deliberate(
|
|
279
|
+
question: str,
|
|
280
|
+
context: str = "",
|
|
281
|
+
max_rounds: int = 3,
|
|
282
|
+
mode: str = "dialogue",
|
|
283
|
+
require_unanimous: bool = True,
|
|
284
|
+
save_path: Optional[str] = None,
|
|
285
|
+
) -> Dict[str, Any]:
|
|
286
|
+
"""
|
|
287
|
+
Run a multi-round deliberation across all configured AI models.
|
|
288
|
+
|
|
289
|
+
Modes:
|
|
290
|
+
- "debate": Long-form essays, models respond to each other's full arguments (3 rounds default)
|
|
291
|
+
- "dialogue": Short conversational turns, models build on each other like a group chat (6 rounds default)
|
|
292
|
+
|
|
293
|
+
Returns the full deliberation transcript + final verdict.
|
|
294
|
+
"""
|
|
295
|
+
DELIBERATION_DIR.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
|
|
297
|
+
config = get_models_config()
|
|
298
|
+
enabled_models = {k: v for k, v in config.items() if v.get("enabled")}
|
|
299
|
+
|
|
300
|
+
if len(enabled_models) < 2:
|
|
301
|
+
return {
|
|
302
|
+
"error": "Need at least 2 AI models for deliberation.",
|
|
303
|
+
"configured": list(enabled_models.keys()),
|
|
304
|
+
"missing": {k: f"Set {v.get('env_key', 'key')}" for k, v in config.items() if not v.get("enabled")},
|
|
305
|
+
"tip": "Set API key environment variables or create ~/.delimit/models.json",
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
model_ids = list(enabled_models.keys())
|
|
309
|
+
|
|
310
|
+
# Dialogue mode uses more rounds with shorter responses
|
|
311
|
+
if mode == "dialogue" and max_rounds == 3:
|
|
312
|
+
max_rounds = 6
|
|
313
|
+
|
|
314
|
+
transcript = {
|
|
315
|
+
"question": question,
|
|
316
|
+
"context": context,
|
|
317
|
+
"mode": mode,
|
|
318
|
+
"models": model_ids,
|
|
319
|
+
"started_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
320
|
+
"rounds": [],
|
|
321
|
+
"thread": [], # flat conversation thread for dialogue mode
|
|
322
|
+
"unanimous": False,
|
|
323
|
+
"final_verdict": None,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if mode == "dialogue":
|
|
327
|
+
system_prompt = (
|
|
328
|
+
"You are in a group chat with other AI models. Keep responses to 2-4 sentences. "
|
|
329
|
+
"Be direct and conversational — this is a discussion, not an essay. "
|
|
330
|
+
"Build on what others said. Disagree specifically if you disagree. "
|
|
331
|
+
"When you're ready to agree, say VERDICT: AGREE. "
|
|
332
|
+
"If you disagree, say VERDICT: DISAGREE — [why in one sentence]."
|
|
333
|
+
)
|
|
334
|
+
else:
|
|
335
|
+
system_prompt = (
|
|
336
|
+
"You are participating in a structured multi-model deliberation with other AI models. "
|
|
337
|
+
"You will see other models' exact responses and must engage with their specific arguments. "
|
|
338
|
+
"At the END of your response, you MUST include exactly one of these lines:\n"
|
|
339
|
+
"VERDICT: AGREE\n"
|
|
340
|
+
"VERDICT: DISAGREE — [one sentence reason]\n"
|
|
341
|
+
"VERDICT: AGREE WITH MODIFICATIONS — [one sentence modification]\n"
|
|
342
|
+
"Do not hedge. Take a clear position."
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
full_prompt = f"{context}\n\nQUESTION:\n{question}" if context else question
|
|
346
|
+
|
|
347
|
+
# Round 1: Independent responses
|
|
348
|
+
logger.info(f"Deliberation Round 1 ({mode} mode): Independent responses")
|
|
349
|
+
round1 = {"round": 1, "type": "independent", "responses": {}}
|
|
350
|
+
|
|
351
|
+
for model_id in model_ids:
|
|
352
|
+
if mode == "dialogue":
|
|
353
|
+
# Shorter initial prompt for dialogue
|
|
354
|
+
r1_prompt = f"{full_prompt}\n\nGive your initial take in 2-4 sentences. Don't write an essay."
|
|
355
|
+
else:
|
|
356
|
+
r1_prompt = full_prompt
|
|
357
|
+
response = _call_model(model_id, enabled_models[model_id], r1_prompt, system_prompt)
|
|
358
|
+
round1["responses"][model_id] = response
|
|
359
|
+
# Build flat thread
|
|
360
|
+
transcript["thread"].append({"model": model_id, "round": 1, "text": response})
|
|
361
|
+
logger.info(f" {model_id}: {len(response)} chars")
|
|
362
|
+
|
|
363
|
+
transcript["rounds"].append(round1)
|
|
364
|
+
|
|
365
|
+
# Subsequent rounds: Models see each other's responses
|
|
366
|
+
for round_num in range(2, max_rounds + 1):
|
|
367
|
+
logger.info(f"Deliberation Round {round_num} ({mode})")
|
|
368
|
+
round_data = {"round": round_num, "type": "deliberation", "responses": {}}
|
|
369
|
+
prev = transcript["rounds"][-1]["responses"]
|
|
370
|
+
|
|
371
|
+
for model_id in model_ids:
|
|
372
|
+
if mode == "dialogue":
|
|
373
|
+
# Dialogue: show the full conversation thread so far
|
|
374
|
+
thread_text = f"Topic: {question}\n\nConversation so far:\n"
|
|
375
|
+
for entry in transcript["thread"]:
|
|
376
|
+
name = enabled_models.get(entry["model"], {}).get("name", entry["model"])
|
|
377
|
+
thread_text += f"\n[{name}]: {entry['text']}\n"
|
|
378
|
+
thread_text += (
|
|
379
|
+
f"\nYour turn ({enabled_models[model_id]['name']}). "
|
|
380
|
+
f"Respond in 2-4 sentences to the conversation above. "
|
|
381
|
+
f"If you agree with the emerging consensus, say VERDICT: AGREE. "
|
|
382
|
+
f"If not, push back specifically."
|
|
383
|
+
)
|
|
384
|
+
cross_prompt = thread_text
|
|
385
|
+
else:
|
|
386
|
+
# Debate: show other models' full responses from last round
|
|
387
|
+
others_text = ""
|
|
388
|
+
for other_id in model_ids:
|
|
389
|
+
if other_id != model_id:
|
|
390
|
+
others_text += (
|
|
391
|
+
f"\n=== {enabled_models[other_id]['name'].upper()}'S EXACT RESPONSE "
|
|
392
|
+
f"(Round {round_num - 1}) ===\n"
|
|
393
|
+
f"{prev[other_id]}\n"
|
|
394
|
+
)
|
|
395
|
+
cross_prompt = (
|
|
396
|
+
f"DELIBERATION ROUND {round_num}\n\n"
|
|
397
|
+
f"Original question: {question}\n"
|
|
398
|
+
f"{others_text}\n"
|
|
399
|
+
f"Respond to the other models' SPECIFIC arguments. "
|
|
400
|
+
f"Quote them directly if you disagree. "
|
|
401
|
+
f"End with VERDICT: AGREE / DISAGREE / AGREE WITH MODIFICATIONS."
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
response = _call_model(model_id, enabled_models[model_id], cross_prompt, system_prompt)
|
|
405
|
+
round_data["responses"][model_id] = response
|
|
406
|
+
transcript["thread"].append({"model": model_id, "round": round_num, "text": response})
|
|
407
|
+
|
|
408
|
+
transcript["rounds"].append(round_data)
|
|
409
|
+
|
|
410
|
+
# Check for unanimous agreement
|
|
411
|
+
all_agree = True
|
|
412
|
+
for model_id in model_ids:
|
|
413
|
+
resp = round_data["responses"][model_id].upper()
|
|
414
|
+
if "VERDICT:" in resp:
|
|
415
|
+
verdict_part = resp.split("VERDICT:")[-1].strip()
|
|
416
|
+
agrees = verdict_part.startswith("AGREE")
|
|
417
|
+
if not agrees:
|
|
418
|
+
all_agree = False
|
|
419
|
+
else:
|
|
420
|
+
all_agree = False # No verdict = no agreement
|
|
421
|
+
|
|
422
|
+
if all_agree:
|
|
423
|
+
transcript["unanimous"] = True
|
|
424
|
+
transcript["final_verdict"] = "UNANIMOUS AGREEMENT"
|
|
425
|
+
transcript["agreed_at_round"] = round_num
|
|
426
|
+
break
|
|
427
|
+
else:
|
|
428
|
+
# Max rounds reached
|
|
429
|
+
transcript["final_verdict"] = "MAX ROUNDS REACHED"
|
|
430
|
+
for model_id in model_ids:
|
|
431
|
+
resp = transcript["rounds"][-1]["responses"][model_id].upper()
|
|
432
|
+
verdict = "unknown"
|
|
433
|
+
if "VERDICT:" in resp:
|
|
434
|
+
verdict_part = resp.split("VERDICT:")[-1].strip()
|
|
435
|
+
verdict = "agree" if verdict_part.startswith("AGREE") else "disagree"
|
|
436
|
+
transcript[f"{model_id}_final"] = verdict
|
|
9
437
|
|
|
10
|
-
|
|
11
|
-
raise ImportError("Deliberation engine requires Pro module. Run: npx delimit-cli setup")
|
|
438
|
+
transcript["completed_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
12
439
|
|
|
13
|
-
|
|
14
|
-
|
|
440
|
+
# Save transcript
|
|
441
|
+
save_to = save_path
|
|
442
|
+
if not save_to:
|
|
443
|
+
ts = time.strftime("%Y%m%d_%H%M%S")
|
|
444
|
+
save_to = str(DELIBERATION_DIR / f"deliberation_{ts}.json")
|
|
15
445
|
|
|
16
|
-
|
|
17
|
-
|
|
446
|
+
Path(save_to).parent.mkdir(parents=True, exist_ok=True)
|
|
447
|
+
Path(save_to).write_text(json.dumps(transcript, indent=2))
|
|
448
|
+
transcript["saved_to"] = save_to
|
|
18
449
|
|
|
19
|
-
|
|
20
|
-
raise ImportError("Deliberation engine requires Pro module.")
|
|
450
|
+
return transcript
|