aline-ai 0.6.2__py3-none-any.whl → 0.6.3__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.
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/METADATA +1 -1
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/RECORD +28 -30
- realign/__init__.py +1 -1
- realign/adapters/__init__.py +0 -3
- realign/cli.py +0 -1
- realign/commands/export_shares.py +154 -226
- realign/commands/watcher.py +28 -79
- realign/config.py +1 -47
- realign/dashboard/app.py +2 -8
- realign/dashboard/screens/event_detail.py +0 -3
- realign/dashboard/screens/session_detail.py +0 -1
- realign/dashboard/widgets/config_panel.py +109 -249
- realign/dashboard/widgets/events_table.py +71 -128
- realign/dashboard/widgets/sessions_table.py +76 -135
- realign/dashboard/widgets/watcher_panel.py +0 -2
- realign/db/sqlite_db.py +1 -2
- realign/events/event_summarizer.py +76 -35
- realign/events/session_summarizer.py +73 -32
- realign/hooks.py +383 -574
- realign/llm_client.py +201 -520
- realign/triggers/__init__.py +0 -2
- realign/triggers/next_turn_trigger.py +4 -5
- realign/triggers/registry.py +1 -4
- realign/watcher_core.py +3 -35
- realign/adapters/antigravity.py +0 -159
- realign/triggers/antigravity_trigger.py +0 -140
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/top_level.txt +0 -0
realign/llm_client.py
CHANGED
|
@@ -6,14 +6,13 @@ This module provides a centralized interface for calling LLM providers (Claude,
|
|
|
6
6
|
with configurable models and parameters.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import os
|
|
10
9
|
import sys
|
|
11
10
|
import time
|
|
12
11
|
import json
|
|
13
12
|
import logging
|
|
14
13
|
import tempfile
|
|
15
14
|
from datetime import datetime
|
|
16
|
-
from typing import Optional, Tuple, Dict, Any
|
|
15
|
+
from typing import Optional, Tuple, Dict, Any
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
|
|
19
18
|
# Setup dedicated LLM logger
|
|
@@ -67,563 +66,245 @@ def _setup_llm_call_logger():
|
|
|
67
66
|
return _llm_call_logger
|
|
68
67
|
|
|
69
68
|
|
|
70
|
-
def
|
|
71
|
-
system_prompt: str,
|
|
72
|
-
user_prompt: str,
|
|
73
|
-
provider: Optional[str] = None,
|
|
74
|
-
model: Optional[str] = None,
|
|
75
|
-
max_tokens: Optional[int] = None,
|
|
76
|
-
temperature: Optional[float] = None,
|
|
77
|
-
json_mode: bool = False,
|
|
78
|
-
debug_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
79
|
-
purpose: str = "generic",
|
|
80
|
-
silent: bool = False,
|
|
81
|
-
) -> Tuple[Optional[str], Optional[str]]:
|
|
69
|
+
def extract_json(response_text: str) -> Dict[str, Any]:
|
|
82
70
|
"""
|
|
83
|
-
|
|
71
|
+
Extract JSON object from a raw LLM response, handling Markdown fences.
|
|
72
|
+
Uses strict=False to tolerate control characters in JSON strings.
|
|
84
73
|
|
|
85
74
|
Args:
|
|
86
|
-
|
|
87
|
-
user_prompt: User prompt
|
|
88
|
-
provider: LLM provider ("auto", "claude", "openai"), None = read from config
|
|
89
|
-
model: Model name, None = use default from config
|
|
90
|
-
max_tokens: Maximum tokens to generate, None = use default
|
|
91
|
-
temperature: Temperature parameter, None = use default
|
|
92
|
-
json_mode: Enable JSON mode (OpenAI only)
|
|
93
|
-
debug_callback: Debug callback function
|
|
94
|
-
purpose: Purpose string for logging
|
|
95
|
-
silent: If True, suppress progress messages to stderr
|
|
75
|
+
response_text: Raw LLM response
|
|
96
76
|
|
|
97
77
|
Returns:
|
|
98
|
-
|
|
78
|
+
Parsed JSON dict
|
|
99
79
|
|
|
100
80
|
Raises:
|
|
101
|
-
|
|
81
|
+
json.JSONDecodeError: If JSON parsing fails
|
|
102
82
|
"""
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
if not response_text:
|
|
84
|
+
raise json.JSONDecodeError("Empty response", "", 0)
|
|
105
85
|
|
|
106
|
-
|
|
86
|
+
json_str = response_text.strip()
|
|
107
87
|
|
|
108
|
-
#
|
|
109
|
-
if
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
""
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def _collect_responses_output_text(response: Any) -> str:
|
|
139
|
-
"""
|
|
140
|
-
Aggregate textual content from OpenAI responses API objects.
|
|
141
|
-
"""
|
|
142
|
-
parts: List[str] = []
|
|
143
|
-
output_items = getattr(response, "output", None) or []
|
|
144
|
-
for item in output_items:
|
|
145
|
-
content = getattr(item, "content", None) or []
|
|
146
|
-
for block in content:
|
|
147
|
-
text = getattr(block, "text", None)
|
|
148
|
-
if text:
|
|
149
|
-
parts.append(text)
|
|
150
|
-
text = "".join(parts).strip()
|
|
151
|
-
if text:
|
|
152
|
-
return text
|
|
153
|
-
fallback = getattr(response, "output_text", "") or ""
|
|
154
|
-
return fallback.strip()
|
|
155
|
-
|
|
156
|
-
def _emit_debug(payload: Dict[str, Any]) -> None:
|
|
157
|
-
"""Emit debug event if callback is provided."""
|
|
158
|
-
if not debug_callback:
|
|
159
|
-
return
|
|
160
|
-
try:
|
|
161
|
-
debug_callback(payload)
|
|
162
|
-
except Exception:
|
|
163
|
-
logger.debug("LLM debug callback failed for payload=%s", payload, exc_info=True)
|
|
88
|
+
# Remove markdown code fences if present
|
|
89
|
+
if "```json" in response_text:
|
|
90
|
+
json_start = response_text.find("```json") + 7
|
|
91
|
+
json_end = response_text.find("```", json_start)
|
|
92
|
+
if json_end != -1:
|
|
93
|
+
json_str = response_text[json_start:json_end].strip()
|
|
94
|
+
elif "```" in response_text:
|
|
95
|
+
json_start = response_text.find("```") + 3
|
|
96
|
+
json_end = response_text.find("```", json_start)
|
|
97
|
+
if json_end != -1:
|
|
98
|
+
json_str = response_text[json_start:json_end].strip()
|
|
99
|
+
|
|
100
|
+
if not json_str:
|
|
101
|
+
raise json.JSONDecodeError("No JSON content found", response_text, 0)
|
|
102
|
+
|
|
103
|
+
# Use strict=False to allow control characters in JSON strings
|
|
104
|
+
return json.loads(json_str, strict=False)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def call_llm_cloud(
|
|
108
|
+
task: str,
|
|
109
|
+
payload: Dict[str, Any],
|
|
110
|
+
custom_prompt: Optional[str] = None,
|
|
111
|
+
preset_id: Optional[str] = None,
|
|
112
|
+
timeout: float = 60.0,
|
|
113
|
+
silent: bool = False,
|
|
114
|
+
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
|
|
115
|
+
"""
|
|
116
|
+
Call LLM via Aline server (cloud proxy).
|
|
164
117
|
|
|
165
|
-
|
|
118
|
+
This function sends structured task/payload to the server which handles
|
|
119
|
+
the LLM call, protecting API keys and prompts from being exposed to clients.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
task: Task type ("summary" | "metadata" | "session_summary" | "event_summary" | "ui_metadata")
|
|
123
|
+
payload: Task-specific data dict
|
|
124
|
+
custom_prompt: Optional user custom prompt override (from ~/.aline/prompts/)
|
|
125
|
+
preset_id: Optional preset ID for ui_metadata task
|
|
126
|
+
timeout: Request timeout in seconds
|
|
127
|
+
silent: If True, suppress progress messages to stderr
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
(model_name, result_dict) or (None, None) on failure
|
|
131
|
+
"""
|
|
132
|
+
# Setup logging
|
|
166
133
|
call_logger = _setup_llm_call_logger()
|
|
167
134
|
call_start_time = time.time()
|
|
168
135
|
call_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
169
136
|
|
|
170
|
-
# Log call initiation
|
|
137
|
+
# Log call initiation
|
|
171
138
|
call_logger.info("=" * 80)
|
|
172
|
-
call_logger.info(
|
|
139
|
+
call_logger.info("LLM CLOUD CALL INITIATED")
|
|
173
140
|
call_logger.info(f"Timestamp: {call_timestamp}")
|
|
174
|
-
call_logger.info(f"
|
|
175
|
-
call_logger.info(f"
|
|
176
|
-
call_logger.info(f"
|
|
177
|
-
call_logger.info(f"Max Tokens: {max_tokens}")
|
|
178
|
-
call_logger.info(f"Temperature: {temperature}")
|
|
179
|
-
call_logger.info(f"JSON Mode: {json_mode}")
|
|
141
|
+
call_logger.info(f"Task: {task}")
|
|
142
|
+
call_logger.info(f"Payload keys: {list(payload.keys())}")
|
|
143
|
+
call_logger.info(f"Custom prompt: {'yes' if custom_prompt else 'no'}")
|
|
180
144
|
call_logger.info("-" * 80)
|
|
181
|
-
call_logger.info(f"SYSTEM PROMPT:\n{system_prompt}")
|
|
182
|
-
call_logger.info("-" * 80)
|
|
183
|
-
call_logger.info(f"USER PROMPT:\n{user_prompt}")
|
|
184
|
-
call_logger.info("-" * 80)
|
|
185
|
-
|
|
186
|
-
try_claude = provider in ("auto", "claude")
|
|
187
|
-
try_openai = provider in ("auto", "openai")
|
|
188
|
-
|
|
189
|
-
_emit_debug(
|
|
190
|
-
{
|
|
191
|
-
"event": "llm_prompt",
|
|
192
|
-
"target_provider": provider,
|
|
193
|
-
"system_prompt": system_prompt,
|
|
194
|
-
"user_prompt": user_prompt,
|
|
195
|
-
"provider_options": {
|
|
196
|
-
"try_claude": try_claude,
|
|
197
|
-
"try_openai": try_openai,
|
|
198
|
-
},
|
|
199
|
-
"purpose": purpose,
|
|
200
|
-
}
|
|
201
|
-
)
|
|
202
145
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
146
|
+
# Check if httpx is available
|
|
147
|
+
try:
|
|
148
|
+
import httpx
|
|
149
|
+
except ImportError:
|
|
150
|
+
logger.error("httpx not available for cloud LLM calls")
|
|
207
151
|
if not silent:
|
|
208
|
-
print("
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
start_time = time.time()
|
|
213
|
-
client = anthropic.Anthropic(api_key=anthropic_key)
|
|
214
|
-
|
|
215
|
-
# Use model parameter if specified, otherwise read from config/env
|
|
216
|
-
claude_model = model or config.llm_anthropic_model
|
|
217
|
-
|
|
218
|
-
response = client.messages.create(
|
|
219
|
-
model=claude_model,
|
|
220
|
-
max_tokens=max_tokens,
|
|
221
|
-
temperature=temperature,
|
|
222
|
-
system=system_prompt,
|
|
223
|
-
messages=[{"role": "user", "content": user_prompt}],
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
elapsed = time.time() - start_time
|
|
227
|
-
response_text = response.content[0].text.strip()
|
|
228
|
-
logger.info(f"Claude API success: {len(response_text)} chars in {elapsed:.2f}s")
|
|
229
|
-
logger.debug(f"Claude response: {response_text[:200]}...")
|
|
230
|
-
_emit_debug(
|
|
231
|
-
{
|
|
232
|
-
"event": "llm_response",
|
|
233
|
-
"provider": "anthropic",
|
|
234
|
-
"model": claude_model,
|
|
235
|
-
"elapsed_seconds": elapsed,
|
|
236
|
-
"raw_response": response_text,
|
|
237
|
-
"purpose": purpose,
|
|
238
|
-
}
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
# Log successful response
|
|
242
|
-
total_elapsed = time.time() - call_start_time
|
|
243
|
-
call_logger.info(f"LLM CALL SUCCEEDED")
|
|
244
|
-
call_logger.info(f"Provider: Anthropic (Claude)")
|
|
245
|
-
call_logger.info(f"Model: {claude_model}")
|
|
246
|
-
call_logger.info(f"Elapsed Time: {elapsed:.2f}s")
|
|
247
|
-
call_logger.info(f"Total Time: {total_elapsed:.2f}s")
|
|
248
|
-
call_logger.info(f"Response Length: {len(response_text)} chars")
|
|
249
|
-
call_logger.info("-" * 80)
|
|
250
|
-
call_logger.info(f"RESPONSE:\n{response_text}")
|
|
251
|
-
call_logger.info("=" * 80 + "\n")
|
|
252
|
-
|
|
253
|
-
return claude_model, response_text
|
|
254
|
-
|
|
255
|
-
except ImportError:
|
|
256
|
-
logger.warning("Anthropic package not installed")
|
|
257
|
-
if provider == "claude":
|
|
258
|
-
if not silent:
|
|
259
|
-
print(" ❌ Anthropic package not installed", file=sys.stderr)
|
|
260
|
-
total_elapsed = time.time() - call_start_time
|
|
261
|
-
call_logger.error(f"LLM CALL FAILED")
|
|
262
|
-
call_logger.error(f"Provider: Anthropic (Claude)")
|
|
263
|
-
call_logger.error(f"Reason: Anthropic package not installed")
|
|
264
|
-
call_logger.error(f"Total Time: {total_elapsed:.2f}s")
|
|
265
|
-
call_logger.error("=" * 80 + "\n")
|
|
266
|
-
return None, None
|
|
267
|
-
if not silent:
|
|
268
|
-
print(
|
|
269
|
-
" ❌ Anthropic package not installed, trying OpenAI...",
|
|
270
|
-
file=sys.stderr,
|
|
271
|
-
)
|
|
272
|
-
except Exception as e:
|
|
273
|
-
error_msg = str(e)
|
|
274
|
-
logger.error(f"Claude API error: {error_msg}", exc_info=True)
|
|
275
|
-
if provider == "claude":
|
|
276
|
-
if not silent:
|
|
277
|
-
if "authentication" in error_msg.lower() or "invalid" in error_msg.lower():
|
|
278
|
-
print(
|
|
279
|
-
f" ❌ Anthropic authentication failed (check API key)",
|
|
280
|
-
file=sys.stderr,
|
|
281
|
-
)
|
|
282
|
-
elif "quota" in error_msg.lower() or "credit" in error_msg.lower():
|
|
283
|
-
print(f" ❌ Anthropic quota/credit issue", file=sys.stderr)
|
|
284
|
-
else:
|
|
285
|
-
print(f" ❌ Anthropic API error: {e}", file=sys.stderr)
|
|
286
|
-
total_elapsed = time.time() - call_start_time
|
|
287
|
-
call_logger.error(f"LLM CALL FAILED")
|
|
288
|
-
call_logger.error(f"Provider: Anthropic (Claude)")
|
|
289
|
-
call_logger.error(f"Reason: {error_msg}")
|
|
290
|
-
call_logger.error(f"Total Time: {total_elapsed:.2f}s")
|
|
291
|
-
call_logger.error("=" * 80 + "\n")
|
|
292
|
-
return None, None
|
|
293
|
-
if not silent:
|
|
294
|
-
print(f" ❌ Anthropic API error: {e}, trying OpenAI...", file=sys.stderr)
|
|
152
|
+
print(" ❌ httpx package not installed", file=sys.stderr)
|
|
153
|
+
call_logger.error("LLM CLOUD CALL FAILED: httpx not installed")
|
|
154
|
+
call_logger.error("=" * 80 + "\n")
|
|
155
|
+
return None, None
|
|
295
156
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
" ❌ Anthropic API key not configured in config file",
|
|
302
|
-
file=sys.stderr,
|
|
303
|
-
)
|
|
304
|
-
return None, None
|
|
157
|
+
# Get auth token
|
|
158
|
+
try:
|
|
159
|
+
from .auth import get_access_token, is_logged_in
|
|
160
|
+
except ImportError:
|
|
161
|
+
logger.error("Auth module not available")
|
|
305
162
|
if not silent:
|
|
306
|
-
print("
|
|
163
|
+
print(" ❌ Auth module not available", file=sys.stderr)
|
|
164
|
+
call_logger.error("LLM CLOUD CALL FAILED: auth module not available")
|
|
165
|
+
call_logger.error("=" * 80 + "\n")
|
|
166
|
+
return None, None
|
|
307
167
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if try_openai and openai_key:
|
|
311
|
-
logger.debug("OPENAI_API_KEY found, attempting OpenAI")
|
|
168
|
+
if not is_logged_in():
|
|
169
|
+
logger.debug("Not logged in, cannot use cloud LLM")
|
|
312
170
|
if not silent:
|
|
313
|
-
print("
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
start_time = time.time()
|
|
318
|
-
client = openai.OpenAI(api_key=openai_key)
|
|
319
|
-
|
|
320
|
-
# Use model parameter if specified, otherwise read from config/env
|
|
321
|
-
openai_model = model or config.llm_openai_model
|
|
322
|
-
use_responses_api = _should_use_openai_responses(openai_model)
|
|
323
|
-
|
|
324
|
-
def _call_openai_chat_completion() -> Tuple[Any, str]:
|
|
325
|
-
use_completion_tokens = False
|
|
326
|
-
temperature_value = temperature
|
|
327
|
-
bad_request_error = getattr(openai, "BadRequestError", Exception)
|
|
328
|
-
last_error: Optional[Exception] = None
|
|
329
|
-
for _ in range(3):
|
|
330
|
-
completion_kwargs = {
|
|
331
|
-
"model": openai_model,
|
|
332
|
-
"messages": [
|
|
333
|
-
{"role": "system", "content": system_prompt},
|
|
334
|
-
{"role": "user", "content": user_prompt},
|
|
335
|
-
],
|
|
336
|
-
"temperature": temperature_value,
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
# Add JSON mode if requested
|
|
340
|
-
if json_mode:
|
|
341
|
-
completion_kwargs["response_format"] = {"type": "json_object"}
|
|
342
|
-
|
|
343
|
-
token_key = "max_completion_tokens" if use_completion_tokens else "max_tokens"
|
|
344
|
-
completion_kwargs[token_key] = max_tokens
|
|
345
|
-
|
|
346
|
-
try:
|
|
347
|
-
completion = client.chat.completions.create(**completion_kwargs)
|
|
348
|
-
text = (completion.choices[0].message.content or "").strip()
|
|
349
|
-
return completion, text
|
|
350
|
-
except bad_request_error as bad_request:
|
|
351
|
-
error_msg = str(bad_request)
|
|
352
|
-
last_error = bad_request
|
|
353
|
-
needs_completion_tokens = (
|
|
354
|
-
"max_tokens" in error_msg and "max_completion_tokens" in error_msg
|
|
355
|
-
)
|
|
356
|
-
needs_default_temp = (
|
|
357
|
-
"temperature" in error_msg
|
|
358
|
-
and "default (1)" in error_msg
|
|
359
|
-
and abs(temperature_value - 1) > 1e-6
|
|
360
|
-
)
|
|
361
|
-
if needs_completion_tokens and not use_completion_tokens:
|
|
362
|
-
use_completion_tokens = True
|
|
363
|
-
logger.info(
|
|
364
|
-
"OpenAI model %s requires max_completion_tokens; retrying request",
|
|
365
|
-
openai_model,
|
|
366
|
-
)
|
|
367
|
-
print(
|
|
368
|
-
" ⓘ Retrying OpenAI call with max_completion_tokens...",
|
|
369
|
-
file=sys.stderr,
|
|
370
|
-
)
|
|
371
|
-
continue
|
|
372
|
-
if needs_default_temp:
|
|
373
|
-
temperature_value = 1.0
|
|
374
|
-
logger.info(
|
|
375
|
-
"OpenAI model %s requires default temperature; retrying request",
|
|
376
|
-
openai_model,
|
|
377
|
-
)
|
|
378
|
-
print(
|
|
379
|
-
" ⓘ Retrying OpenAI call with temperature=1...",
|
|
380
|
-
file=sys.stderr,
|
|
381
|
-
)
|
|
382
|
-
continue
|
|
383
|
-
raise
|
|
384
|
-
raise last_error or RuntimeError(
|
|
385
|
-
"Failed to obtain OpenAI response after multiple attempts"
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
def _call_openai_responses_api() -> Tuple[Any, str]:
|
|
389
|
-
def _int_env(name: str, default: int) -> int:
|
|
390
|
-
value = os.getenv(name)
|
|
391
|
-
if not value:
|
|
392
|
-
return default
|
|
393
|
-
try:
|
|
394
|
-
return max(1, int(value))
|
|
395
|
-
except ValueError:
|
|
396
|
-
return default
|
|
397
|
-
|
|
398
|
-
def _float_env(name: str, default: float) -> float:
|
|
399
|
-
value = os.getenv(name)
|
|
400
|
-
if not value:
|
|
401
|
-
return default
|
|
402
|
-
try:
|
|
403
|
-
return float(value)
|
|
404
|
-
except ValueError:
|
|
405
|
-
return default
|
|
406
|
-
|
|
407
|
-
max_output_tokens = _int_env("REALIGN_OPENAI_MAX_OUTPUT_TOKENS", max_tokens)
|
|
408
|
-
reasoning_effort = os.getenv("REALIGN_OPENAI_REASONING_EFFORT", "medium").strip()
|
|
409
|
-
responses_temperature = _float_env(
|
|
410
|
-
"REALIGN_OPENAI_RESPONSES_TEMPERATURE", temperature
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
inputs: List[Dict[str, str]] = []
|
|
414
|
-
if system_prompt:
|
|
415
|
-
inputs.append({"role": "developer", "content": system_prompt})
|
|
416
|
-
inputs.append({"role": "user", "content": user_prompt})
|
|
417
|
-
|
|
418
|
-
request_kwargs: Dict[str, Any] = {
|
|
419
|
-
"model": openai_model,
|
|
420
|
-
"input": inputs,
|
|
421
|
-
"max_output_tokens": max_output_tokens,
|
|
422
|
-
"temperature": responses_temperature,
|
|
423
|
-
}
|
|
424
|
-
if reasoning_effort:
|
|
425
|
-
request_kwargs["reasoning"] = {"effort": reasoning_effort}
|
|
426
|
-
|
|
427
|
-
response = client.responses.create(**request_kwargs)
|
|
428
|
-
text = _collect_responses_output_text(response)
|
|
429
|
-
return response, text
|
|
430
|
-
|
|
431
|
-
endpoint_type = "responses" if use_responses_api else "chat.completions"
|
|
432
|
-
if use_responses_api:
|
|
433
|
-
response, response_text = _call_openai_responses_api()
|
|
434
|
-
else:
|
|
435
|
-
response, response_text = _call_openai_chat_completion()
|
|
436
|
-
|
|
437
|
-
elapsed = time.time() - start_time
|
|
438
|
-
response_text = (response_text or "").strip()
|
|
439
|
-
response_model = getattr(response, "model", openai_model)
|
|
440
|
-
logger.info(
|
|
441
|
-
f"OpenAI {endpoint_type} success: {len(response_text)} chars in {elapsed:.2f}s"
|
|
442
|
-
)
|
|
443
|
-
logger.debug(f"OpenAI response: {response_text[:200]}...")
|
|
444
|
-
_emit_debug(
|
|
445
|
-
{
|
|
446
|
-
"event": "llm_response",
|
|
447
|
-
"provider": "openai",
|
|
448
|
-
"model": response_model,
|
|
449
|
-
"elapsed_seconds": elapsed,
|
|
450
|
-
"raw_response": response_text,
|
|
451
|
-
"purpose": purpose,
|
|
452
|
-
"endpoint": endpoint_type,
|
|
453
|
-
"response_status": getattr(response, "status", None),
|
|
454
|
-
}
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
# Log successful response
|
|
458
|
-
total_elapsed = time.time() - call_start_time
|
|
459
|
-
call_logger.info(f"LLM CALL SUCCEEDED")
|
|
460
|
-
call_logger.info(f"Provider: OpenAI (GPT)")
|
|
461
|
-
call_logger.info(f"Model: {response_model}")
|
|
462
|
-
call_logger.info(f"Endpoint: {endpoint_type}")
|
|
463
|
-
call_logger.info(f"Elapsed Time: {elapsed:.2f}s")
|
|
464
|
-
call_logger.info(f"Total Time: {total_elapsed:.2f}s")
|
|
465
|
-
call_logger.info(f"Response Length: {len(response_text)} chars")
|
|
466
|
-
call_logger.info("-" * 80)
|
|
467
|
-
call_logger.info(f"RESPONSE:\n{response_text}")
|
|
468
|
-
call_logger.info("=" * 80 + "\n")
|
|
469
|
-
|
|
470
|
-
return response_model, response_text
|
|
471
|
-
|
|
472
|
-
except ImportError:
|
|
473
|
-
logger.warning("OpenAI package not installed")
|
|
474
|
-
if not silent:
|
|
475
|
-
print(" ❌ OpenAI package not installed", file=sys.stderr)
|
|
476
|
-
total_elapsed = time.time() - call_start_time
|
|
477
|
-
call_logger.error(f"LLM CALL FAILED")
|
|
478
|
-
call_logger.error(f"Provider: OpenAI (GPT)")
|
|
479
|
-
call_logger.error(f"Reason: OpenAI package not installed")
|
|
480
|
-
call_logger.error(f"Total Time: {total_elapsed:.2f}s")
|
|
481
|
-
call_logger.error("=" * 80 + "\n")
|
|
482
|
-
return None, None
|
|
483
|
-
except Exception as e:
|
|
484
|
-
error_msg = str(e)
|
|
485
|
-
logger.error(f"OpenAI API error: {error_msg}", exc_info=True)
|
|
486
|
-
if not silent:
|
|
487
|
-
if "authentication" in error_msg.lower():
|
|
488
|
-
print(
|
|
489
|
-
" ❌ OpenAI authentication failed (check API key)",
|
|
490
|
-
file=sys.stderr,
|
|
491
|
-
)
|
|
492
|
-
elif "quota" in error_msg.lower() or "billing" in error_msg.lower():
|
|
493
|
-
print(" ❌ OpenAI quota/billing issue", file=sys.stderr)
|
|
494
|
-
else:
|
|
495
|
-
print(f" ❌ OpenAI API error: {e}", file=sys.stderr)
|
|
496
|
-
total_elapsed = time.time() - call_start_time
|
|
497
|
-
call_logger.error(f"LLM CALL FAILED")
|
|
498
|
-
call_logger.error(f"Provider: OpenAI (GPT)")
|
|
499
|
-
call_logger.error(f"Reason: {error_msg}")
|
|
500
|
-
call_logger.error(f"Total Time: {total_elapsed:.2f}s")
|
|
501
|
-
call_logger.error("=" * 80 + "\n")
|
|
502
|
-
return None, None
|
|
171
|
+
print(" ❌ Not logged in to Aline cloud", file=sys.stderr)
|
|
172
|
+
call_logger.error("LLM CLOUD CALL FAILED: not logged in")
|
|
173
|
+
call_logger.error("=" * 80 + "\n")
|
|
174
|
+
return None, None
|
|
503
175
|
|
|
504
|
-
|
|
505
|
-
|
|
176
|
+
access_token = get_access_token()
|
|
177
|
+
if not access_token:
|
|
178
|
+
logger.warning("Failed to get access token")
|
|
506
179
|
if not silent:
|
|
507
|
-
print(" ❌
|
|
180
|
+
print(" ❌ Failed to get access token", file=sys.stderr)
|
|
181
|
+
call_logger.error("LLM CLOUD CALL FAILED: no access token")
|
|
182
|
+
call_logger.error("=" * 80 + "\n")
|
|
508
183
|
return None, None
|
|
509
184
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
print(" ❌ No LLM API keys configured", file=sys.stderr)
|
|
513
|
-
|
|
514
|
-
# Log failure
|
|
515
|
-
total_elapsed = time.time() - call_start_time
|
|
516
|
-
call_logger.error(f"LLM CALL FAILED")
|
|
517
|
-
call_logger.error(f"Reason: No LLM API keys available")
|
|
518
|
-
call_logger.error(f"Provider: {provider}")
|
|
519
|
-
call_logger.error(f"Total Time: {total_elapsed:.2f}s")
|
|
520
|
-
call_logger.error("=" * 80 + "\n")
|
|
521
|
-
|
|
522
|
-
return None, None
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
def call_llm_json(
|
|
526
|
-
system_prompt: str,
|
|
527
|
-
user_prompt: str,
|
|
528
|
-
provider: Optional[str] = None,
|
|
529
|
-
model: Optional[str] = None,
|
|
530
|
-
max_tokens: Optional[int] = None,
|
|
531
|
-
temperature: Optional[float] = None,
|
|
532
|
-
debug_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
533
|
-
purpose: str = "generic",
|
|
534
|
-
silent: bool = False,
|
|
535
|
-
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
|
|
536
|
-
"""
|
|
537
|
-
Call LLM and parse JSON response.
|
|
185
|
+
# Get backend URL from config
|
|
186
|
+
from .config import ReAlignConfig
|
|
538
187
|
|
|
539
|
-
|
|
540
|
-
|
|
188
|
+
config = ReAlignConfig.load()
|
|
189
|
+
backend_url = config.share_backend_url or "https://realign-server.vercel.app"
|
|
190
|
+
endpoint = f"{backend_url}/api/llm/invoke"
|
|
191
|
+
|
|
192
|
+
# Build request body
|
|
193
|
+
request_body: Dict[str, Any] = {
|
|
194
|
+
"task": task,
|
|
195
|
+
"payload": payload,
|
|
196
|
+
}
|
|
197
|
+
if custom_prompt:
|
|
198
|
+
request_body["custom_prompt"] = custom_prompt
|
|
199
|
+
if preset_id:
|
|
200
|
+
request_body["preset_id"] = preset_id
|
|
201
|
+
|
|
202
|
+
if not silent:
|
|
203
|
+
print(f" → Calling Aline cloud LLM ({task})...", file=sys.stderr)
|
|
204
|
+
|
|
205
|
+
call_logger.info(f"Endpoint: {endpoint}")
|
|
206
|
+
call_logger.info(f"Request body: {json.dumps(request_body, ensure_ascii=False)[:2000]}")
|
|
207
|
+
call_logger.info("-" * 80)
|
|
541
208
|
|
|
542
|
-
|
|
543
|
-
(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
debug_callback=debug_callback,
|
|
554
|
-
purpose=purpose,
|
|
555
|
-
silent=silent,
|
|
556
|
-
)
|
|
209
|
+
try:
|
|
210
|
+
start_time = time.time()
|
|
211
|
+
response = httpx.post(
|
|
212
|
+
endpoint,
|
|
213
|
+
json=request_body,
|
|
214
|
+
headers={
|
|
215
|
+
"Authorization": f"Bearer {access_token}",
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
},
|
|
218
|
+
timeout=timeout,
|
|
219
|
+
)
|
|
557
220
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
f" ⚠️ LLM returned empty response (purpose={purpose})",
|
|
562
|
-
file=sys.stderr,
|
|
563
|
-
)
|
|
564
|
-
return model_name, None
|
|
221
|
+
elapsed = time.time() - start_time
|
|
222
|
+
call_logger.info(f"Response status: {response.status_code}")
|
|
223
|
+
call_logger.info(f"Response time: {elapsed:.2f}s")
|
|
565
224
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
file=sys.stderr,
|
|
575
|
-
)
|
|
576
|
-
print(
|
|
577
|
-
f" ⚠️ Response text (last 500 chars): ...{response_text[-500:]}",
|
|
578
|
-
file=sys.stderr,
|
|
579
|
-
)
|
|
580
|
-
return model_name, None
|
|
581
|
-
|
|
582
|
-
if not isinstance(parsed, dict):
|
|
583
|
-
logger.warning("LLM JSON was not an object (purpose=%s): %r", purpose, type(parsed))
|
|
584
|
-
if not silent:
|
|
585
|
-
print(
|
|
586
|
-
f" ⚠️ LLM returned {type(parsed)} instead of dict (purpose={purpose})",
|
|
587
|
-
file=sys.stderr,
|
|
588
|
-
)
|
|
589
|
-
return model_name, None
|
|
225
|
+
# Handle HTTP errors
|
|
226
|
+
if response.status_code == 401:
|
|
227
|
+
logger.warning("Cloud LLM authentication failed")
|
|
228
|
+
if not silent:
|
|
229
|
+
print(" ❌ Cloud LLM authentication failed", file=sys.stderr)
|
|
230
|
+
call_logger.error("LLM CLOUD CALL FAILED: authentication error (401)")
|
|
231
|
+
call_logger.error("=" * 80 + "\n")
|
|
232
|
+
return None, None
|
|
590
233
|
|
|
591
|
-
|
|
234
|
+
if response.status_code == 429:
|
|
235
|
+
logger.warning("Cloud LLM rate limited")
|
|
236
|
+
if not silent:
|
|
237
|
+
print(" ❌ Cloud LLM rate limited", file=sys.stderr)
|
|
238
|
+
call_logger.error("LLM CLOUD CALL FAILED: rate limited (429)")
|
|
239
|
+
call_logger.error("=" * 80 + "\n")
|
|
240
|
+
return None, None
|
|
592
241
|
|
|
242
|
+
if response.status_code >= 500:
|
|
243
|
+
logger.warning(f"Cloud LLM server error: {response.status_code}")
|
|
244
|
+
if not silent:
|
|
245
|
+
print(f" ❌ Cloud LLM server error ({response.status_code})", file=sys.stderr)
|
|
246
|
+
call_logger.error(f"LLM CLOUD CALL FAILED: server error ({response.status_code})")
|
|
247
|
+
call_logger.error("=" * 80 + "\n")
|
|
248
|
+
return None, None
|
|
593
249
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
Uses strict=False to tolerate control characters in JSON strings.
|
|
250
|
+
# Parse response
|
|
251
|
+
data = response.json()
|
|
252
|
+
call_logger.info(f"Response body: {json.dumps(data, ensure_ascii=False)[:2000]}")
|
|
598
253
|
|
|
599
|
-
|
|
600
|
-
|
|
254
|
+
if not data.get("success"):
|
|
255
|
+
error_msg = data.get("error", "Unknown error")
|
|
256
|
+
logger.warning(f"Cloud LLM call failed: {error_msg}")
|
|
257
|
+
if not silent:
|
|
258
|
+
print(f" ❌ Cloud LLM error: {error_msg}", file=sys.stderr)
|
|
259
|
+
call_logger.error(f"LLM CLOUD CALL FAILED: {error_msg}")
|
|
260
|
+
call_logger.error("=" * 80 + "\n")
|
|
261
|
+
return None, None
|
|
601
262
|
|
|
602
|
-
|
|
603
|
-
|
|
263
|
+
model_name = data.get("model", "cloud")
|
|
264
|
+
result = data.get("result", {})
|
|
604
265
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
266
|
+
# Log success
|
|
267
|
+
total_elapsed = time.time() - call_start_time
|
|
268
|
+
call_logger.info("LLM CLOUD CALL SUCCEEDED")
|
|
269
|
+
call_logger.info(f"Provider: Cloud ({model_name})")
|
|
270
|
+
call_logger.info(f"Task: {task}")
|
|
271
|
+
call_logger.info(f"Elapsed Time: {elapsed:.2f}s")
|
|
272
|
+
call_logger.info(f"Total Time: {total_elapsed:.2f}s")
|
|
273
|
+
call_logger.info("-" * 80)
|
|
274
|
+
call_logger.info(f"RESULT: {json.dumps(result, ensure_ascii=False)}")
|
|
275
|
+
call_logger.info("=" * 80 + "\n")
|
|
610
276
|
|
|
611
|
-
|
|
277
|
+
if not silent:
|
|
278
|
+
print(f" ✅ Cloud LLM success ({model_name})", file=sys.stderr)
|
|
612
279
|
|
|
613
|
-
|
|
614
|
-
if "```json" in response_text:
|
|
615
|
-
json_start = response_text.find("```json") + 7
|
|
616
|
-
json_end = response_text.find("```", json_start)
|
|
617
|
-
if json_end != -1:
|
|
618
|
-
json_str = response_text[json_start:json_end].strip()
|
|
619
|
-
elif "```" in response_text:
|
|
620
|
-
json_start = response_text.find("```") + 3
|
|
621
|
-
json_end = response_text.find("```", json_start)
|
|
622
|
-
if json_end != -1:
|
|
623
|
-
json_str = response_text[json_start:json_end].strip()
|
|
280
|
+
return model_name, result
|
|
624
281
|
|
|
625
|
-
|
|
626
|
-
|
|
282
|
+
except httpx.TimeoutException:
|
|
283
|
+
logger.warning(f"Cloud LLM request timed out after {timeout}s")
|
|
284
|
+
if not silent:
|
|
285
|
+
print(f" ❌ Cloud LLM request timed out", file=sys.stderr)
|
|
286
|
+
total_elapsed = time.time() - call_start_time
|
|
287
|
+
call_logger.error(f"LLM CLOUD CALL FAILED: timeout after {timeout}s")
|
|
288
|
+
call_logger.error(f"Total Time: {total_elapsed:.2f}s")
|
|
289
|
+
call_logger.error("=" * 80 + "\n")
|
|
290
|
+
return None, None
|
|
627
291
|
|
|
628
|
-
|
|
629
|
-
|
|
292
|
+
except httpx.RequestError as e:
|
|
293
|
+
logger.warning(f"Cloud LLM request error: {e}")
|
|
294
|
+
if not silent:
|
|
295
|
+
print(f" ❌ Cloud LLM connection error", file=sys.stderr)
|
|
296
|
+
total_elapsed = time.time() - call_start_time
|
|
297
|
+
call_logger.error(f"LLM CLOUD CALL FAILED: request error - {e}")
|
|
298
|
+
call_logger.error(f"Total Time: {total_elapsed:.2f}s")
|
|
299
|
+
call_logger.error("=" * 80 + "\n")
|
|
300
|
+
return None, None
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
logger.error(f"Cloud LLM unexpected error: {e}", exc_info=True)
|
|
304
|
+
if not silent:
|
|
305
|
+
print(f" ❌ Cloud LLM error: {e}", file=sys.stderr)
|
|
306
|
+
total_elapsed = time.time() - call_start_time
|
|
307
|
+
call_logger.error(f"LLM CLOUD CALL FAILED: unexpected error - {e}")
|
|
308
|
+
call_logger.error(f"Total Time: {total_elapsed:.2f}s")
|
|
309
|
+
call_logger.error("=" * 80 + "\n")
|
|
310
|
+
return None, None
|