aline-ai 0.6.2__py3-none-any.whl → 0.6.4__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 (40) hide show
  1. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/METADATA +1 -1
  2. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/RECORD +38 -37
  3. realign/__init__.py +1 -1
  4. realign/adapters/__init__.py +0 -3
  5. realign/adapters/codex.py +14 -9
  6. realign/cli.py +42 -236
  7. realign/codex_detector.py +72 -32
  8. realign/codex_home.py +85 -0
  9. realign/codex_terminal_linker.py +172 -0
  10. realign/commands/__init__.py +2 -2
  11. realign/commands/add.py +89 -9
  12. realign/commands/doctor.py +495 -0
  13. realign/commands/export_shares.py +154 -226
  14. realign/commands/init.py +66 -4
  15. realign/commands/watcher.py +30 -80
  16. realign/config.py +9 -46
  17. realign/dashboard/app.py +7 -11
  18. realign/dashboard/screens/event_detail.py +0 -3
  19. realign/dashboard/screens/session_detail.py +0 -1
  20. realign/dashboard/tmux_manager.py +129 -4
  21. realign/dashboard/widgets/config_panel.py +175 -241
  22. realign/dashboard/widgets/events_table.py +71 -128
  23. realign/dashboard/widgets/sessions_table.py +77 -136
  24. realign/dashboard/widgets/terminal_panel.py +349 -27
  25. realign/dashboard/widgets/watcher_panel.py +0 -2
  26. realign/db/sqlite_db.py +77 -2
  27. realign/events/event_summarizer.py +76 -35
  28. realign/events/session_summarizer.py +73 -32
  29. realign/hooks.py +334 -647
  30. realign/llm_client.py +201 -520
  31. realign/triggers/__init__.py +0 -2
  32. realign/triggers/next_turn_trigger.py +4 -5
  33. realign/triggers/registry.py +1 -4
  34. realign/watcher_core.py +53 -35
  35. realign/adapters/antigravity.py +0 -159
  36. realign/triggers/antigravity_trigger.py +0 -140
  37. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/WHEEL +0 -0
  38. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/entry_points.txt +0 -0
  39. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/licenses/LICENSE +0 -0
  40. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.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, Callable, List
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 call_llm(
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
- Unified LLM calling function.
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
- system_prompt: System prompt
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
- (model_name, response_text) or (None, None) on failure
78
+ Parsed JSON dict
99
79
 
100
80
  Raises:
101
- No exceptions raised - returns (None, None) on failure
81
+ json.JSONDecodeError: If JSON parsing fails
102
82
  """
103
- # Load configuration
104
- from .config import ReAlignConfig
83
+ if not response_text:
84
+ raise json.JSONDecodeError("Empty response", "", 0)
105
85
 
106
- config = ReAlignConfig.load()
86
+ json_str = response_text.strip()
107
87
 
108
- # Resolve provider from config if not specified
109
- if provider is None:
110
- provider = config.llm_provider
111
-
112
- # Resolve default parameters from config if not specified
113
- if max_tokens is None:
114
- max_tokens = config.llm_max_tokens
115
- if temperature is None:
116
- temperature = config.llm_temperature
117
-
118
- def _should_use_openai_responses(model_name: str) -> bool:
119
- """
120
- Decide if the OpenAI responses/reasoning API should be used for this model.
121
- """
122
- # Check explicit override
123
- if config.llm_openai_use_responses:
124
- return True
125
-
126
- # Also check environment variable for backwards compatibility
127
- override = os.getenv("REALIGN_OPENAI_USE_RESPONSES", "").strip().lower()
128
- if override in ("1", "true", "yes"):
129
- return True
130
- if override in ("0", "false", "no"):
131
- return False
132
- if not model_name:
133
- return False
134
- # Auto-detect: use responses API for GPT-5+ models
135
- lowered = model_name.lower()
136
- return lowered.startswith("gpt-5")
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
- # Setup detailed logging
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 with all parameters
137
+ # Log call initiation
171
138
  call_logger.info("=" * 80)
172
- call_logger.info(f"LLM CALL INITIATED")
139
+ call_logger.info("LLM CLOUD CALL INITIATED")
173
140
  call_logger.info(f"Timestamp: {call_timestamp}")
174
- call_logger.info(f"Purpose: {purpose}")
175
- call_logger.info(f"Provider: {provider}")
176
- call_logger.info(f"Model: {model or 'default from config'}")
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
- # Try Claude
204
- anthropic_key = config.anthropic_api_key
205
- if try_claude and anthropic_key:
206
- logger.debug("ANTHROPIC_API_KEY found, attempting Claude")
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(" Trying Anthropic (Claude)...", file=sys.stderr)
209
- try:
210
- import anthropic
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
- elif try_claude:
297
- logger.debug("Anthropic API key not configured in config file")
298
- if provider == "claude":
299
- if not silent:
300
- print(
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("Anthropic API key not configured, trying OpenAI...", file=sys.stderr)
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
- # Try OpenAI
309
- openai_key = config.openai_api_key
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("Trying OpenAI (GPT)...", file=sys.stderr)
314
- try:
315
- import openai
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
- elif try_openai:
505
- logger.debug("OpenAI API key not configured in config file")
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(" ❌ OpenAI API key not configured in config file", file=sys.stderr)
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
- logger.warning(f"No LLM API keys available (provider: {provider})")
511
- if provider == "auto" and not silent:
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
- Args:
540
- Same as call_llm()
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
- Returns:
543
- (model_name, json_dict) where json_dict is None on failure
544
- """
545
- model_name, response_text = call_llm(
546
- system_prompt=system_prompt,
547
- user_prompt=user_prompt,
548
- provider=provider,
549
- model=model,
550
- max_tokens=max_tokens,
551
- temperature=temperature,
552
- json_mode=True, # Always enable JSON mode for this function
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
- if not response_text:
559
- if not silent:
560
- print(
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
- try:
567
- parsed = extract_json(response_text)
568
- except Exception as e:
569
- logger.warning("Failed to parse LLM JSON (purpose=%s): %s", purpose, e, exc_info=True)
570
- if not silent:
571
- print(f" ⚠️ Failed to parse JSON (purpose={purpose}): {e}", file=sys.stderr)
572
- print(
573
- f" ⚠️ Response text (first 500 chars): {response_text[:500]}",
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
- return model_name, parsed
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
- def extract_json(response_text: str) -> Dict[str, Any]:
595
- """
596
- Extract JSON object from a raw LLM response, handling Markdown fences.
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
- Args:
600
- response_text: Raw LLM response
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
- Returns:
603
- Parsed JSON dict
263
+ model_name = data.get("model", "cloud")
264
+ result = data.get("result", {})
604
265
 
605
- Raises:
606
- json.JSONDecodeError: If JSON parsing fails
607
- """
608
- if not response_text:
609
- raise json.JSONDecodeError("Empty response", "", 0)
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
- json_str = response_text.strip()
277
+ if not silent:
278
+ print(f" ✅ Cloud LLM success ({model_name})", file=sys.stderr)
612
279
 
613
- # Remove markdown code fences if present
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
- if not json_str:
626
- raise json.JSONDecodeError("No JSON content found", response_text, 0)
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
- # Use strict=False to allow control characters in JSON strings
629
- return json.loads(json_str, strict=False)
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