luaf 1.2.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.
LUAF.py ADDED
@@ -0,0 +1,2145 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+ import functools, hashlib, json, os, pickle, queue, random, re, subprocess, sys, tempfile, time, uuid
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Any, Iterable, Optional
7
+ import requests
8
+ from dotenv import load_dotenv
9
+ from loguru import logger
10
+ try:
11
+ from luaf_tui import create_luaf_app
12
+ except ImportError:
13
+ create_luaf_app = None
14
+ try:
15
+ from toolbox.templates import get_template as _get_template
16
+ except ImportError:
17
+ _get_template = None
18
+ try:
19
+ from planner import plan_from_topic_and_search as _plan_from_topic_and_search
20
+ from executor import execute_plan as _execute_plan
21
+ except ImportError:
22
+ _plan_from_topic_and_search = None
23
+ _execute_plan = None
24
+ try:
25
+ from swarms import Agent as SwarmsAgent
26
+ except ImportError:
27
+ try:
28
+ from swarms.structs.agent import Agent as SwarmsAgent
29
+ except ImportError:
30
+ SwarmsAgent = None
31
+ try:
32
+ from swarms_client import SwarmsClient as _SwarmsClient
33
+ except ImportError:
34
+ _SwarmsClient = None
35
+ _ReactAgent = None
36
+ try:
37
+ from swarms.agents.react_agent import ReactAgent as _ReactAgent
38
+ except ImportError:
39
+ try:
40
+ from swarms.agents import ReactAgent as _ReactAgent
41
+ except ImportError:
42
+ pass
43
+ try:
44
+ from openclaw_controller import run_social_autonomy as _run_social_autonomy
45
+ except ImportError:
46
+ _run_social_autonomy = None
47
+ from luaf_publish import publish_agent, get_private_key_from_env, get_creator_pubkey, get_solana_balance, load_agents_registry as _load_agents_registry, append_agent_to_registry, claim_fees, run_delayed_claim_pass
48
+ try:
49
+ from luaf_x_post import add_agent_to_x_pending as _add_agent_to_x_pending, maybe_post_x_batch as _maybe_post_x_batch, drain_x_queue as _drain_x_queue, is_x_post_enabled as _is_x_post_enabled
50
+ except ImportError:
51
+ _add_agent_to_x_pending = None
52
+ _maybe_post_x_batch = None
53
+ _drain_x_queue = None
54
+ _is_x_post_enabled = None
55
+ try:
56
+ from luaf_profiles import list_profiles as _list_profiles, get_default_profile as _get_default_profile_impl
57
+ except ImportError:
58
+ _list_profiles = None
59
+ _get_default_profile_impl = None
60
+ _REPO_ROOT = Path(__file__).resolve().parents[1]
61
+ _LUAF_DIR = Path(__file__).resolve().parent
62
+ load_dotenv(_REPO_ROOT / '.env')
63
+ load_dotenv(Path.cwd() / '.env')
64
+ if (os.environ.get('LUAF_LOG_FILE', '1') or '').strip().lower() not in ('0', 'false', 'no'):
65
+ _log_dir = _LUAF_DIR / 'logs'
66
+ _log_dir.mkdir(parents=True, exist_ok=True)
67
+ _log_file = _log_dir / 'luaf.log'
68
+ try:
69
+ logger.add(_log_file, format='{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}', level=0, rotation='10 MB', retention='7 days', encoding='utf-8')
70
+ except Exception:
71
+ pass
72
+
73
+ def _env_bool(name: str, default: str='0') -> bool:
74
+ return (os.environ.get(name, default) or '').strip().lower() in ('1', 'true', 'yes')
75
+
76
+ def _env_int(name: str, default: int, lo: int=1, hi: int=999999) -> int:
77
+ try:
78
+ return max(lo, min(hi, int(os.environ.get(name, str(default)))))
79
+ except (TypeError, ValueError):
80
+ return default
81
+
82
+ def _env_float(name: str, default: float, lo: float=0.0, hi: float=2.0) -> float:
83
+ try:
84
+ return max(lo, min(hi, float(os.environ.get(name, str(default)))))
85
+ except (TypeError, ValueError):
86
+ return default
87
+
88
+ # CLI theme (OpenClaw palette + Codex clarity). Respect NO_COLOR and --no-color.
89
+ _cli_no_color: bool = False
90
+
91
+ def _cli_theme_no_color() -> bool:
92
+ """Return True if CLI styling should be disabled (NO_COLOR or --no-color)."""
93
+ if _cli_no_color:
94
+ return True
95
+ return (os.environ.get('NO_COLOR') or '').strip() != ''
96
+
97
+ def set_cli_no_color(value: bool) -> None:
98
+ global _cli_no_color
99
+ _cli_no_color = bool(value)
100
+
101
+ def _style_heading(s: str) -> str:
102
+ """Accent style for headings, labels, command names."""
103
+ if _cli_theme_no_color():
104
+ return s
105
+ return f'\033[1;38;5;202m{s}\033[0m'
106
+
107
+ def _style_success(s: str) -> str:
108
+ if _cli_theme_no_color():
109
+ return s
110
+ return f'\033[32m{s}\033[0m'
111
+
112
+ def _style_warn(s: str) -> str:
113
+ if _cli_theme_no_color():
114
+ return s
115
+ return f'\033[33m{s}\033[0m'
116
+
117
+ def _style_error(s: str) -> str:
118
+ if _cli_theme_no_color():
119
+ return s
120
+ return f'\033[31m{s}\033[0m'
121
+
122
+ def _style_muted(s: str) -> str:
123
+ if _cli_theme_no_color():
124
+ return s
125
+ return f'\033[2;37m{s}\033[0m'
126
+
127
+ def _style_info(s: str) -> str:
128
+ if _cli_theme_no_color():
129
+ return s
130
+ return f'\033[36m{s}\033[0m'
131
+
132
+ def _style_accent(s: str) -> str:
133
+ """Primary highlight (accentBright)."""
134
+ if _cli_theme_no_color():
135
+ return s
136
+ return f'\033[1;38;5;208m{s}\033[0m'
137
+
138
+ def _str_from_result(r: Any) -> str:
139
+ if isinstance(r, str):
140
+ return r.strip()
141
+ if isinstance(r, dict):
142
+ return (r.get('output') or r.get('content') or r.get('message') or str(r)).strip()
143
+ return str(r).strip()
144
+
145
+ def _resp_json(resp: Any) -> dict:
146
+ try:
147
+ return resp.json() if resp.text.strip() else {}
148
+ except json.JSONDecodeError:
149
+ return {'_raw': resp.text[:500] if resp.text else ''}
150
+ _DEFAULT_TOPIC = ''
151
+ _env_topic = os.environ.get('LUAF_TOPIC')
152
+ TOPIC = '' if _env_topic == '' else (_env_topic or _DEFAULT_TOPIC).strip() or _DEFAULT_TOPIC
153
+ MAX_AGENTS = _env_int('LUAF_MAX_AGENTS', 1)
154
+ DRY_RUN = _env_bool('LUAF_DRY_RUN', '1')
155
+ LLM_MODEL = os.environ.get('LUAF_LLM_MODEL', 'gpt-4.1')
156
+ LLM_TEMPERATURE = _env_float('LUAF_LLM_TEMPERATURE', 0.9, 0.0, 2.0)
157
+ DUCKDUCKGO_MAX_RESULTS = 20
158
+ USE_MULTIHOP_WEB_RAG = _env_bool('LUAF_USE_MULTIHOP_WEB_RAG', '0')
159
+ RAG_MAX_HOPS = _env_int('LUAF_RAG_MAX_HOPS', 3, lo=1, hi=10)
160
+ RAG_CONVERGE_THRESHOLD = _env_float('LUAF_RAG_CONVERGE_THRESHOLD', 0.7, 0.0, 1.0)
161
+ RAG_TOTAL_K = _env_int('LUAF_RAG_TOTAL_K', 20, lo=1, hi=100)
162
+ RAG_DDG_PER_HOP = _env_int('LUAF_RAG_DDG_PER_HOP', 15, lo=1, hi=50)
163
+ USE_KEYLESS_API_SEARCH = _env_bool('LUAF_KEYLESS_API_SEARCH', '1')
164
+ USE_RUN_IN_NEW_TERMINAL = _env_bool('LUAF_RUN_IN_NEW_TERMINAL', '1')
165
+ SWARMS_API_KEY_FALLBACK = 'sk-2ca8ca93580702aff03e1991da20aa364d9e3d4e11fffd14c651145a2226d012'
166
+ AGENTS_REGISTRY_PATH = Path(__file__).resolve().parent / 'tokenized_agents.json'
167
+ _generated_agent_dir_env = (os.environ.get('LUAF_GENERATED_AGENTS_DIR') or 'generated_agents').strip()
168
+ GENERATED_AGENTS_DIR = _LUAF_DIR / _generated_agent_dir_env if _generated_agent_dir_env not in ('.', '') else _LUAF_DIR
169
+ CLAIM_FEES_AFTER_RUN = True
170
+ PERSISTENT_TARGET_SOL = _env_float('LUAF_PERSISTENT_TARGET_SOL', 10.0, 0.0, 1000000.0)
171
+ PERSISTENT_TOPIC_SOURCE = (os.environ.get('LUAF_PERSISTENT_TOPIC_SOURCE') or 'single').strip().lower()
172
+ if PERSISTENT_TOPIC_SOURCE not in ('single', 'env', 'file'):
173
+ PERSISTENT_TOPIC_SOURCE = 'single'
174
+ PERSISTENT_MIN_SOL_TO_TOKENIZE = _env_float('LUAF_MIN_SOL_TO_TOKENIZE', 0.05, 0.0, 1000000.0)
175
+ CLAIM_DELAY_HOURS = _env_float('LUAF_CLAIM_DELAY_HOURS', 24.0, 0.0, 8760.0)
176
+ PERSISTENT_LOOP_SLEEP_SECONDS = _env_int('LUAF_PERSISTENT_LOOP_SLEEP_SECONDS', 0, 0, 86400)
177
+ SOLANA_RPC_URL = (os.environ.get('LUAF_SOLANA_RPC_URL') or 'https://api.mainnet-beta.solana.com').strip()
178
+ PERSISTENT_RUN_TASK_ENV = 'LUAF_PERSISTENT_RUN_TASK'
179
+ try:
180
+ _q = json.loads((_LUAF_DIR / 'luaf_quality.json').read_text(encoding='utf-8'))
181
+ except Exception:
182
+ _q = {}
183
+ DESIGN_ANGLES = tuple(_q.get('design_angles', ('backtesting', 'best practices', 'tutorial / step-by-step')))
184
+ SEARCH_VARIANT_SUFFIXES = tuple(_q.get('search_variant_suffixes', ('best practices', 'tutorial', 'guide', '2026', 'overview')))
185
+ QUALITY_PACKAGES_BY_CATEGORY = _q.get('quality_packages_by_category', {'core': ['swarms', 'loguru'], 'http': ['requests', 'httpx'], 'search': ['ddgs'], 'data_analytics': ['pandas', 'numpy']})
186
+ QUALITY_CATEGORY_KEYWORDS = _q.get('quality_category_keywords', {'core': [], 'http': ['api', 'rest', 'http'], 'search': ['search', 'duckduckgo'], 'data_analytics': ['data', 'analytics', 'pandas', 'numpy', 'trading']})
187
+
188
+ def _categories_for_topic(topic: str) -> list[str]:
189
+ t = (topic or '').lower().strip()
190
+ if not t:
191
+ return ['core', 'http', 'search', 'data_analytics']
192
+ chosen: set[str] = {'core'}
193
+ for cat, keywords in QUALITY_CATEGORY_KEYWORDS.items():
194
+ if cat == 'core':
195
+ continue
196
+ if any((k in t for k in keywords)):
197
+ chosen.add(cat)
198
+ return sorted(chosen)
199
+
200
+ def _format_quality_packages_for_topic(topic: str) -> str:
201
+ categories = _categories_for_topic(topic)
202
+ lines = ['## Required quality packages (mandatory)', 'You MUST use at least one package from each category below in your agent. List them in requirements and use them in the code. No toy implementations—use these to build a solid, production-quality agent.', '']
203
+ for cat in categories:
204
+ packs = QUALITY_PACKAGES_BY_CATEGORY.get(cat, [])
205
+ if packs:
206
+ lines.append(f'- **{cat}**: ' + ', '.join(packs))
207
+ lines.append('')
208
+ lines.append('Select packages that fit the topic; use more than the minimum when they add value. Every agent must use at least: swarms (core) and loguru (core), plus packages from at least two other listed categories.')
209
+ return '\n'.join(lines)
210
+ FINAL_PAYLOAD_FILENAME = 'final_agent_payload.json'
211
+ WORKSPACE_DIR = os.environ.get('WORKSPACE_DIR', str(Path(__file__).resolve().parent / 'agent_workspace'))
212
+ MAX_STEPS = _env_int('LUAF_MAX_STEPS', 3)
213
+ DESIGNER_MAX_LOOPS = _env_int('LUAF_DESIGNER_MAX_LOOPS', 2, lo=1, hi=20)
214
+ VALIDATION_TIMEOUT = _env_int('LUAF_VALIDATION_TIMEOUT', 600, lo=5)
215
+ LLM_HTTP_TIMEOUT = 1200
216
+ DESIGNER_AGENT_ARCHITECTURE = (os.environ.get('LUAF_DESIGNER_AGENT_ARCHITECTURE') or 'agent').strip().lower()
217
+ if DESIGNER_AGENT_ARCHITECTURE not in ('agent', 'react'):
218
+ DESIGNER_AGENT_ARCHITECTURE = 'agent'
219
+ DESIGNER_USE_DIRECT_API = _env_bool('LUAF_DESIGNER_USE_DIRECT_API', '1')
220
+ USE_PLANNER = _env_bool('LUAF_USE_PLANNER', '1')
221
+ USE_DESIGNER = _env_bool('LUAF_USE_DESIGNER', '1')
222
+ SWARMS_AGENT_DOCS = "\nGenerated code MUST use swarms: from swarms import Agent; Agent(agent_name=str, agent_description=str, system_prompt=str, model_name=str, max_loops=int|'auto'); result = agent.run(task). No stubs, no placeholders. Cloud API: POST https://api.swarms.world/v1/agent/completions with agent_config and task.\n"
223
+ REQUIRED_PAYLOAD_KEYS = frozenset({'name', 'agent', 'description', 'language', 'requirements', 'useCases', 'tags', 'is_free', 'ticker'})
224
+ PUBLICATION_OUTPUT_FORMAT_FRAGMENT = '''
225
+ ## JSON output format (mandatory for publication)
226
+ Your entire response must be the single JSON object: no characters before the opening { or after the closing }. Output must be instantly publication-ready (no post-processing needed).
227
+
228
+ Forbidden: Any text, reasoning, or explanation before the opening {. Any text, summary, or "Done" after the closing }. Markdown code fences (```json or ```). The key private_key. Placeholder or empty values for required keys.
229
+
230
+ Output valid JSON only; no trailing commas. Suggested key order: name, ticker, description, agent, useCases, tags, requirements, language, is_free.
231
+
232
+ Required top-level keys (exactly these; no others):
233
+ - name (string)
234
+ - ticker (string, short uppercase)
235
+ - description (string)
236
+ - agent (string: full Python code; literal newlines in the string are fine)
237
+ - useCases (array of {"title": string, "description": string}); at least 3 items
238
+ - tags (string, comma-separated; no comma inside a tag)
239
+ - requirements (array of {"package": string, "installation": string}); MUST include {"package": "swarms", "installation": "pip install swarms"}
240
+ - language (string)
241
+ - is_free (boolean true only)
242
+
243
+ Do NOT include private_key. Do NOT wrap the output in ``` or any other formatting.
244
+ '''.strip()
245
+ from luaf_designer import parse_agent_payload as _parse_agent_payload_impl, retrieve_similar_exemplars
246
+ DESIGNER_EXEMPLARS_PATH = _LUAF_DIR / 'designer_exemplars.jsonl'
247
+ def parse_agent_payload(raw: str) -> dict[str, Any]:
248
+ return _parse_agent_payload_impl(raw, REQUIRED_PAYLOAD_KEYS)
249
+ def _retrieve_similar_exemplars(topic: str, search_snippets: str, top_k: int = 3) -> list[str]:
250
+ return retrieve_similar_exemplars(topic, search_snippets, DESIGNER_EXEMPLARS_PATH, top_k)
251
+ _designer_prompt_path = _LUAF_DIR / 'designer_system_prompt.txt'
252
+ DESIGNER_SYSTEM_PROMPT = _designer_prompt_path.read_text(encoding='utf-8') if _designer_prompt_path.exists() else ''
253
+ if not DESIGNER_SYSTEM_PROMPT.strip():
254
+ logger.warning('designer_system_prompt.txt not found next to LUAF.py; designer may not behave as expected')
255
+ PROFILES_DIR = _LUAF_DIR / 'profiles'
256
+ _DEFAULT_TOPIC_PROMPT = 'Generate exactly one concrete, autonomous business idea that is monetizable and tokenizable. It must make money without a frontend: e.g. API usage, token fees, data/arbitrage/sellable output, automated backends—no subscription sites, dashboards, or SaaS UIs. Reply with only that one sentence, no quotes, no explanation, no bullet points.'
257
+ _DEFAULT_PRODUCT_FOCUS = 'Product focus: Tokenized units only; revenue via API usage, token fees, data/arbitrage/sellable output—not via products that need a web frontend (no subscription UI, dashboard, or SaaS customer-facing app).'
258
+ _active_profile: Optional[dict[str, Any]] = None
259
+
260
+
261
+ def _get_default_profile() -> dict[str, Any]:
262
+ """Return the default profile (current designer_system_prompt.txt + default topic/focus)."""
263
+ if _get_default_profile_impl is None:
264
+ return {
265
+ 'id': 'default',
266
+ 'display_name': 'default',
267
+ 'system_prompt': DESIGNER_SYSTEM_PROMPT,
268
+ 'topic_prompt': _DEFAULT_TOPIC_PROMPT,
269
+ 'product_focus': _DEFAULT_PRODUCT_FOCUS,
270
+ }
271
+ return _get_default_profile_impl(_designer_prompt_path, _DEFAULT_TOPIC_PROMPT, _DEFAULT_PRODUCT_FOCUS)
272
+
273
+
274
+ def get_active_profile() -> dict[str, Any]:
275
+ """Return the currently active profile; if none set, return default."""
276
+ global _active_profile
277
+ if _active_profile is not None:
278
+ return _active_profile
279
+ return _get_default_profile()
280
+
281
+
282
+ def _generate_profile_from_keywords(keywords: str) -> Optional[dict[str, Any]]:
283
+ """Call LLM to generate a profile (system_prompt, topic_prompt, product_focus) from keywords. In-memory only; follows same rules (plain text, no MD, backend monetization). Returns profile dict or None on failure."""
284
+ keywords = (keywords or '').strip()[:500]
285
+ if not keywords:
286
+ logger.debug('Profile from keywords: no keywords provided')
287
+ return None
288
+ api_key = os.environ.get('OPENAI_API_KEY', '').strip()
289
+ base_url = (os.environ.get('OPENAI_BASE_URL') or 'https://api.openai.com/v1').strip()
290
+ if not api_key:
291
+ logger.warning('OPENAI_API_KEY not set; cannot generate profile from keywords.')
292
+ return None
293
+ logger.info('Generating profile from keywords: {}', keywords[:80] + ('…' if len(keywords) > 80 else ''))
294
+ system = """You generate a LUAF designer profile from keywords. Your response is parsed by splitting on exact header lines. Any deviation causes rejection.
295
+
296
+ STRICT OUTPUT FORMAT (mandatory):
297
+ - Your first line of the response MUST be exactly: ## SYSTEM_PROMPT
298
+ - Then a blank line, then the full SYSTEM_PROMPT content (plain text only, 300+ lines).
299
+ - Then exactly this line on its own: ## TOPIC_PROMPT
300
+ - Then a blank line, then one paragraph for TOPIC_PROMPT (plain text).
301
+ - Then exactly this line on its own: ## PRODUCT_FOCUS
302
+ - Then a blank line, then one short paragraph for PRODUCT_FOCUS (plain text).
303
+ - Nothing before the first ## SYSTEM_PROMPT. Nothing after the PRODUCT_FOCUS paragraph.
304
+
305
+ Header lines must be exactly these, with no extra characters or spaces:
306
+ ## SYSTEM_PROMPT
307
+ ## TOPIC_PROMPT
308
+ ## PRODUCT_FOCUS
309
+
310
+ Content rules: Plain text only in all three sections. No Markdown inside content (no **, no ##, no bullet lists). No preamble, no "Here is...", no summary after PRODUCT_FOCUS. Revenue must be achievable without a customer-facing web app (API, data, automation, backend only). SYSTEM_PROMPT must be a complete designer system prompt: programming excellence, 300+ lines, utility and monetization, product focus, output rules, process, agent architecture, code quality, listing metadata. It MUST include a "JSON output format (mandatory for publication)" section that specifies: the designer's entire response must be a single JSON object; required top-level keys exactly name, ticker, description, agent, useCases, tags, requirements, language, is_free; agent = full Python code string; useCases = array of {title, description}; requirements = array of {package, installation} including swarms; is_free = boolean true only; no private_key; no markdown fences. TOPIC_PROMPT: one paragraph that generates a single business idea. PRODUCT_FOCUS: one short paragraph for the designer user message."""
311
+ user = f"Generate a LUAF profile for these keywords. Output only the three sections starting with ## SYSTEM_PROMPT as specified: {keywords}"
312
+ debug_path: Optional[Path] = None
313
+ try:
314
+ resp = requests.post(f"{base_url.rstrip('/')}/chat/completions", headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, json={'model': LLM_MODEL, 'messages': [{'role': 'system', 'content': system}, {'role': 'user', 'content': user}], 'temperature': 0.3, 'max_tokens': 8192}, timeout=min(120, LLM_HTTP_TIMEOUT))
315
+ if not resp.ok:
316
+ logger.warning('LLM profile generation failed: {} {}', resp.status_code, resp.text[:200])
317
+ return None
318
+ content = (resp.json().get('choices') or [{}])[0].get('message', {}).get('content') or ''
319
+ if not content.strip():
320
+ logger.warning('LLM profile generation returned empty content')
321
+ return None
322
+ logger.info('LLM profile response received, length={} chars', len(content))
323
+ PROFILES_DIR.mkdir(parents=True, exist_ok=True)
324
+ ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
325
+ safe_kw = re.sub(r'[^\w\-]', '_', keywords[:30]).strip('_') or 'keywords'
326
+ debug_path = PROFILES_DIR / f'generated_keywords_{safe_kw}_{ts}.txt'
327
+ try:
328
+ debug_path.write_text(content, encoding='utf-8')
329
+ logger.debug('Wrote LLM profile response to {}', debug_path)
330
+ except OSError as e:
331
+ logger.warning('Could not write profile debug file {}: {}', debug_path, e)
332
+ normalized = content.strip()
333
+ if normalized.startswith('##'):
334
+ normalized = '\n' + normalized
335
+ parts = re.split(r'\n##\s+(SYSTEM_PROMPT|TOPIC_PROMPT|PRODUCT_FOCUS)\s*\n', normalized, flags=re.IGNORECASE)
336
+ logger.debug('Profile parse: {} parts from header split', len(parts))
337
+ result: dict[str, str] = {}
338
+ i = 1
339
+ while i + 1 < len(parts):
340
+ header = (parts[i] or '').strip().upper()
341
+ body = (parts[i + 1] or '').strip()
342
+ if header == 'SYSTEM_PROMPT':
343
+ result['system_prompt'] = body
344
+ elif header == 'TOPIC_PROMPT':
345
+ result['topic_prompt'] = body
346
+ elif header == 'PRODUCT_FOCUS':
347
+ result['product_focus'] = body
348
+ i += 2
349
+ if not result.get('system_prompt'):
350
+ logger.warning('LLM profile response missing SYSTEM_PROMPT (parse produced {} parts). Raw response saved to: {}', len(parts), debug_path)
351
+ if content.strip():
352
+ logger.debug('Response starts with: {}', repr(content.strip()[:300]))
353
+ return None
354
+ system_prompt = result.get('system_prompt', '')
355
+ if PUBLICATION_OUTPUT_FORMAT_FRAGMENT not in system_prompt:
356
+ system_prompt = system_prompt + '\n\n' + PUBLICATION_OUTPUT_FORMAT_FRAGMENT
357
+ logger.info('Profile from keywords parsed successfully')
358
+ return {
359
+ 'id': 'generated',
360
+ 'display_name': f'Generated: {keywords[:40]}{"…" if len(keywords) > 40 else ""}',
361
+ 'system_prompt': system_prompt,
362
+ 'topic_prompt': result.get('topic_prompt') or None,
363
+ 'product_focus': result.get('product_focus') or None,
364
+ }
365
+ except Exception as e:
366
+ logger.warning('Generate profile from keywords failed: {}', e)
367
+ if debug_path:
368
+ logger.info('Check raw LLM output in {}', debug_path)
369
+ return None
370
+
371
+
372
+ def _ticker_select_cli(options: list[dict[str, Any]], prompt: str = 'Select profile:') -> int:
373
+ """Ls-style ticker-select: list one per line; use questionary if available else number input. Returns index."""
374
+ if not options:
375
+ return 0
376
+ n = len(options)
377
+ try:
378
+ import questionary
379
+ choices = [p.get('display_name', p.get('id', '')) for p in options]
380
+ ans = questionary.select(prompt, choices=choices).ask()
381
+ if ans is None:
382
+ return 0
383
+ for i, p in enumerate(options):
384
+ if p.get('display_name', p.get('id', '')) == ans:
385
+ return i
386
+ return 0
387
+ except ImportError:
388
+ pass
389
+ print(' ' + prompt)
390
+ for i, p in enumerate(options):
391
+ print(f' {i} {p.get("display_name", p.get("id", ""))}')
392
+ try:
393
+ raw = input(' Choice [0]: ').strip() or '0'
394
+ idx = max(0, min(n - 1, int(raw)))
395
+ except (ValueError, EOFError, KeyboardInterrupt):
396
+ idx = 0
397
+ return idx
398
+
399
+
400
+ def run_profile_selection() -> dict[str, Any]:
401
+ """Show ls-style profile ticker-select (or use LUAF_PROFILE); set and return active profile. When stdin is not a TTY, use LUAF_PROFILE or default (no menu)."""
402
+ global _active_profile
403
+ env_id = (os.environ.get('LUAF_PROFILE') or '').strip()
404
+ is_tty = getattr(sys.stdin, 'isatty', lambda: False)()
405
+ if env_id:
406
+ if _list_profiles is not None and PROFILES_DIR.is_dir():
407
+ for p in _list_profiles(PROFILES_DIR):
408
+ if (p.get('id') or '').lower() == env_id.lower():
409
+ _active_profile = p
410
+ logger.info('Profile: {}', p.get('display_name', env_id))
411
+ return _active_profile
412
+ if env_id.lower() == 'default':
413
+ _active_profile = _get_default_profile()
414
+ return _active_profile
415
+ logger.warning('LUAF_PROFILE={} not found; using default profile.', env_id)
416
+ default = _get_default_profile()
417
+ if not is_tty:
418
+ _active_profile = default
419
+ return _active_profile
420
+ options = [default]
421
+ if _list_profiles is not None and PROFILES_DIR.is_dir():
422
+ options = [default] + _list_profiles(PROFILES_DIR)
423
+ options.append({'id': '_generate', 'display_name': 'Generate from keywords...', '_generated_from_keywords': True})
424
+ if len(options) == 1:
425
+ _active_profile = default
426
+ return _active_profile
427
+ idx = _ticker_select_cli(options)
428
+ chosen = options[idx]
429
+ if chosen.get('_generated_from_keywords'):
430
+ try:
431
+ kw = input(' Keywords (e.g. healthcare API, B2B): ').strip() if getattr(sys.stdin, 'isatty', lambda: False)() else ''
432
+ except (EOFError, KeyboardInterrupt):
433
+ kw = ''
434
+ gen = _generate_profile_from_keywords(kw)
435
+ _active_profile = gen if gen else default
436
+ if not gen:
437
+ logger.warning('Using default profile after failed keyword generation.')
438
+ else:
439
+ logger.info('Profile: {}', _active_profile.get('display_name', 'generated'))
440
+ else:
441
+ _active_profile = chosen
442
+ logger.info('Profile: {}', _active_profile.get('display_name', _active_profile.get('id', 'default')))
443
+ return _active_profile
444
+
445
+
446
+ def _search_duckduckgo_impl(query: str, max_results: int) -> str:
447
+ try:
448
+ from ddgs import DDGS
449
+ except ImportError:
450
+ logger.warning('ddgs not installed; pip install ddgs')
451
+ return ''
452
+ try:
453
+ results = list(DDGS().text(query, max_results=max_results))
454
+ except Exception as e:
455
+ logger.warning('DuckDuckGo search failed: {}', e)
456
+ return ''
457
+ return '\n'.join((f"{r.get('title', '')}: {r.get('body', '')}" for r in results)) if results else ''
458
+
459
+ @functools.lru_cache(maxsize=128)
460
+ def _search_duckduckgo_cached(query: str, max_results: int) -> str:
461
+ return _search_duckduckgo_impl(query, max_results)
462
+
463
+ def search_duckduckgo(query: str, max_results: int=10) -> str:
464
+ return _search_duckduckgo_cached((query or '').strip(), max_results)
465
+
466
+ def _append_keyless_api_search(brief: str, snip: str) -> str:
467
+ """When LUAF_KEYLESS_API_SEARCH is enabled, append keyless/public API search results to snip."""
468
+ if not USE_KEYLESS_API_SEARCH or not (brief or '').strip():
469
+ return snip
470
+ extra = search_duckduckgo(f'{(brief or "").strip()} free public API no API key', max_results=DUCKDUCKGO_MAX_RESULTS)
471
+ if not (extra or '').strip():
472
+ return snip
473
+ return (snip or '') + '\n\nKeyless/public API options:\n' + extra
474
+
475
+ def _search_duckduckgo_snippets_list(query: str, max_results: int) -> list[str]:
476
+ try:
477
+ from ddgs import DDGS
478
+ except ImportError:
479
+ return []
480
+ try:
481
+ results = list(DDGS().text((query or '').strip(), max_results=max_results))
482
+ except Exception:
483
+ return []
484
+ return [f"{r.get('title', '')}: {r.get('body', '')}" for r in results if r]
485
+
486
+ def read_design_brief_interactive() -> str:
487
+ brief = (os.environ.get('LUAF_DESIGN_BRIEF') or os.environ.get('LUAF_TOPIC') or '').strip()
488
+ if brief:
489
+ return brief
490
+ if not _env_bool('LUAF_INTERACTIVE', '1') or not getattr(sys.stdin, 'isatty', lambda: False)():
491
+ return TOPIC
492
+ try:
493
+ u = input(f'Business use case or brief (Enter = {TOPIC}): ').strip()
494
+ except EOFError:
495
+ u = ''
496
+ return u or TOPIC
497
+
498
+ def _read_optional_line(prompt: str) -> Optional[str]:
499
+ if not getattr(sys.stdin, 'isatty', lambda: False)():
500
+ return None
501
+ try:
502
+ value = input(prompt).strip()
503
+ return value if value else None
504
+ except (EOFError, KeyboardInterrupt):
505
+ return None
506
+
507
+ def read_optional_name_and_ticker() -> tuple[Optional[str], Optional[str]]:
508
+ if not _env_bool('LUAF_INTERACTIVE', '1'):
509
+ return (None, None)
510
+ name_override = _read_optional_line('Unit name (Enter = auto): ')
511
+ ticker_override = _read_optional_line('Ticker (Enter = auto): ')
512
+ if ticker_override:
513
+ ticker_override = ticker_override.upper()
514
+ return (name_override, ticker_override)
515
+ MIN_AGENT_LINES = 300
516
+
517
+ def _count_substantive_lines(code: str) -> int:
518
+ return len([L for L in (code or '').splitlines() if L.strip() and (not L.strip().startswith('#'))])
519
+
520
+ def _is_skeleton_agent_code(code: str) -> bool:
521
+ if not (code or '').strip():
522
+ return True
523
+ return 'NotImplementedError' in code or _count_substantive_lines(code) < MIN_AGENT_LINES
524
+
525
+ def _skeleton_validation_feedback(code: str) -> str | None:
526
+ if not (code or '').strip():
527
+ return 'Unit code is empty. Provide at least 300 substantive lines (400+ preferred); no stubs or boilerplate.'
528
+ if 'NotImplementedError' in code:
529
+ return 'Unit code contains stubs (NotImplementedError). Remove all stubs; provide a full implementation (at least 300 substantive lines, 400+ preferred).'
530
+ n = _count_substantive_lines(code)
531
+ if n < MIN_AGENT_LINES:
532
+ return f'Unit code has {n} substantive lines; minimum required is {MIN_AGENT_LINES} (400+ preferred). No stubs, boilerplate, examples, or mock data—only fully functioning runnable code.'
533
+ return None
534
+
535
+ def _ask_publish_without_validation() -> bool:
536
+ if not getattr(sys.stdin, 'isatty', lambda: False)():
537
+ return False
538
+ logger.warning('Validation failed. You can validate manually by running the saved unit script (e.g. python <UnitName>.py).')
539
+ try:
540
+ r = input('Publish without validation? [y/N]: ').strip().lower()
541
+ return r in ('y', 'yes')
542
+ except (EOFError, KeyboardInterrupt):
543
+ return False
544
+
545
+ def _save_generated_agent(agent_code: str, name: str, ticker: str, step: int) -> Optional[Path]:
546
+ if not (agent_code or '').strip():
547
+ return None
548
+ part = (ticker or '').strip() or (name or '').strip()
549
+ if part:
550
+ part = re.sub('[^\\w\\-]', '_', part).strip('_') or f'agent_step{step}'
551
+ else:
552
+ part = f'generated_agent_step{step}'
553
+ dest_dir = GENERATED_AGENTS_DIR
554
+ if dest_dir != _LUAF_DIR:
555
+ try:
556
+ dest_dir.mkdir(parents=True, exist_ok=True)
557
+ except OSError as e:
558
+ logger.warning('Could not create generated units dir {}: {}', dest_dir, e)
559
+ return None
560
+ path = dest_dir / f'{part}.py'
561
+ try:
562
+ path.write_text(agent_code, encoding='utf-8')
563
+ logger.info('Saved generated unit to {}', path)
564
+ return path
565
+ except OSError as e:
566
+ logger.warning('Could not save generated unit to {}: {}', path, e)
567
+ return None
568
+ _RE_MODULE_NOT_FOUND = re.compile('ModuleNotFoundError:\\s*No module named\\s+[\'\\"]([a-zA-Z0-9_.-]+)[\'\\"]', re.IGNORECASE)
569
+
570
+ def _parse_missing_module(stderr: str) -> Optional[str]:
571
+ m = _RE_MODULE_NOT_FOUND.search(stderr)
572
+ return m.group(1) if m else None
573
+
574
+ def _pip_install_module(module: str, timeout: int=120) -> tuple[bool, str]:
575
+ try:
576
+ proc = subprocess.run([sys.executable, '-m', 'pip', 'install', '--break-system-packages', module], capture_output=True, timeout=timeout, env=os.environ.copy())
577
+ err = (proc.stderr or b'').decode('utf-8', errors='replace').strip()
578
+ if proc.returncode != 0:
579
+ out = (proc.stdout or b'').decode('utf-8', errors='replace').strip()
580
+ return (False, err or out or f'pip install {module} failed (exit {proc.returncode})')
581
+ return (True, err)
582
+ except subprocess.TimeoutExpired:
583
+ return (False, f'pip install {module} timed out after {timeout}s.')
584
+ except Exception as e:
585
+ return (False, f'pip install {module} failed: {e!s}')
586
+
587
+ def run_agent_code_validation(agent_code: str, timeout: int) -> tuple[bool, str]:
588
+ if not (agent_code or '').strip():
589
+ return (False, 'Validation failed: unit code is empty.')
590
+ code = agent_code
591
+ for old, new in (('raise NotImplementedError("Implement search (e.g. ddgs or public search API)")', 'return ""'), ('raise NotImplementedError("Implement LLM call (OpenAI-compatible or swarms Agent)")', 'return "{}"'), ('raise NotImplementedError("Implement POST to add-agent when USE_PUBLISH is true")', 'return None')):
592
+ code = code.replace(old, new)
593
+ code = re.sub('raise\\s+Exception\\s*\\(\\s*f?\\s*["\\\']Search API failed with status code:\\s*\\{\\s*response\\.status_code\\s*\\}\\s*["\\\']\\s*\\)', 'return getattr(response, "text", "") or "" # 2xx treated as success for validation', code)
594
+ code = re.sub('\\bUSE_SEARCH\\s*=\\s*True\\b', 'USE_SEARCH = False', code, count=1)
595
+ code = re.sub('\\bUSE_LLM\\s*=\\s*True\\b', 'USE_LLM = False', code, count=1)
596
+ code = re.sub('\\bmethod_whitelist\\s*=', 'allowed_methods=', code)
597
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', prefix='luaf_agent_', delete=False, encoding='utf-8') as f:
598
+ f.write(code)
599
+ script_path = f.name
600
+ script_dir = os.path.dirname(script_path)
601
+ for fn, content in (('agent_profile.json', json.dumps({'name': 'validation', 'preferences': {}})),):
602
+ try:
603
+ Path(script_dir).joinpath(fn).write_text(content, encoding='utf-8')
604
+ except OSError:
605
+ pass
606
+ max_import_retries = int(os.environ.get('LUAF_MAX_MISSING_IMPORT_RETRIES', '3'))
607
+ last_fb = ''
608
+ tried_install: set[str] = set()
609
+ _log_cap = int(os.environ.get('LUAF_VALIDATION_LOG_CAP', '2000'))
610
+ try:
611
+ for attempt in range(max_import_retries + 1):
612
+ logger.info('Validation run attempt {}: python {} (timeout={}s, cwd={})', attempt + 1, script_path, timeout, script_dir)
613
+ try:
614
+ proc = subprocess.run([sys.executable, script_path], capture_output=True, timeout=timeout, cwd=script_dir, env=os.environ.copy())
615
+ except subprocess.TimeoutExpired:
616
+ logger.warning('Validation timed out after {}s', timeout)
617
+ return (False, f'Validation failed: timed out after {timeout}s.')
618
+ except Exception as e:
619
+ logger.warning('Validation subprocess error: {}', e)
620
+ return (False, f'Validation failed: {e!s}')
621
+ out = (proc.stdout or b'').decode('utf-8', errors='replace').strip()
622
+ err = (proc.stderr or b'').decode('utf-8', errors='replace').strip()
623
+ last_fb = f"Validation failed (exit {proc.returncode}).\nStdout:\n{out or '(empty)'}\nStderr:\n{err or '(empty)'}"
624
+ logger.info('Validation exit code: {}', proc.returncode)
625
+ if out:
626
+ out_log = out if len(out) <= _log_cap else out[:_log_cap] + '\n... (truncated)'
627
+ logger.info('Validation stdout:\n{}', out_log)
628
+ else:
629
+ logger.info('Validation stdout: (empty)')
630
+ if err:
631
+ err_log = err if len(err) <= _log_cap else err[:_log_cap] + '\n... (truncated)'
632
+ logger.info('Validation stderr:\n{}', err_log)
633
+ else:
634
+ logger.info('Validation stderr: (empty)')
635
+ if proc.returncode == 0:
636
+ logger.info('Validation passed.')
637
+ return (True, '')
638
+ missing = _parse_missing_module(err)
639
+ if not missing or attempt >= max_import_retries or missing in tried_install:
640
+ logger.warning('Validation failed (exit {}). See stdout/stderr above.', proc.returncode)
641
+ return (False, last_fb)
642
+ logger.info("Validation failed with missing import '{}'; attempting pip install and retry.", missing)
643
+ tried_install.add(missing)
644
+ ok, pip_msg = _pip_install_module(missing)
645
+ if not ok:
646
+ logger.warning('pip install {} failed: {}', missing, pip_msg[:500])
647
+ return (False, last_fb)
648
+ logger.info("Installed '{}'; re-running validation.", missing)
649
+ finally:
650
+ try:
651
+ os.unlink(script_path)
652
+ except OSError:
653
+ pass
654
+
655
+ def run_agent_in_new_terminal(script_path: Path | str, task: str, cwd: Optional[str] = None) -> None:
656
+ """Launch the agent script in a new terminal window so the user can observe it. Uses platform-appropriate commands."""
657
+ path = Path(script_path)
658
+ if not path.is_file():
659
+ logger.warning('Cannot run in new terminal: script not found at {}', path)
660
+ return
661
+ task_str = (task or '').strip() or 'Run a quick check.'
662
+ work_dir = cwd or str(path.parent)
663
+ try:
664
+ if sys.platform == 'win32':
665
+ # CREATE_NEW_CONSOLE opens a new console window; the window stays open so output is visible.
666
+ flags = subprocess.CREATE_NEW_CONSOLE if hasattr(subprocess, 'CREATE_NEW_CONSOLE') else 0
667
+ subprocess.Popen(
668
+ [sys.executable, str(path), task_str],
669
+ cwd=work_dir,
670
+ env=os.environ.copy(),
671
+ creationflags=flags,
672
+ )
673
+ logger.info('Launched agent in new terminal: {}', path.name)
674
+ else:
675
+ # Unix: try gnome-terminal, then xterm, then fall back to a background Popen (no new window).
676
+ cmd = [sys.executable, str(path), task_str]
677
+ try:
678
+ subprocess.Popen(
679
+ ['gnome-terminal', '--', sys.executable, str(path), task_str],
680
+ cwd=work_dir,
681
+ env=os.environ.copy(),
682
+ )
683
+ logger.info('Launched agent in new terminal (gnome-terminal): {}', path.name)
684
+ except FileNotFoundError:
685
+ try:
686
+ import shlex
687
+ subprocess.Popen(
688
+ ['xterm', '-e', ' '.join(shlex.quote(str(x)) for x in cmd)],
689
+ cwd=work_dir,
690
+ env=os.environ.copy(),
691
+ )
692
+ logger.info('Launched agent in new terminal (xterm): {}', path.name)
693
+ except FileNotFoundError:
694
+ subprocess.Popen(cmd, cwd=work_dir, env=os.environ.copy())
695
+ logger.info('Launched agent in background (no terminal available): {}', path.name)
696
+ except Exception as e:
697
+ logger.warning('Failed to launch agent in new terminal: {}', e)
698
+
699
+ def run_agent_once(agent_code: str, task: str, timeout: int=VALIDATION_TIMEOUT) -> tuple[bool, str]:
700
+ if not (agent_code or '').strip():
701
+ return (False, 'Unit code is empty.')
702
+ task_str = (task or '').strip() or 'Run a quick check.'
703
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', prefix='luaf_run_', delete=False, encoding='utf-8') as f:
704
+ f.write(agent_code)
705
+ script_path = f.name
706
+ try:
707
+ proc = subprocess.run([sys.executable, script_path, task_str], capture_output=True, timeout=timeout, cwd=os.path.dirname(script_path), env=os.environ.copy())
708
+ out = (proc.stdout or b'').decode('utf-8', errors='replace').strip()
709
+ err = (proc.stderr or b'').decode('utf-8', errors='replace').strip()
710
+ if proc.returncode == 0:
711
+ return (True, out or '')
712
+ return (False, f"Exit {proc.returncode}. Stdout: {out[:500] or '(empty)'}. Stderr: {err[:500] or '(empty)'}")
713
+ except subprocess.TimeoutExpired:
714
+ return (False, f'Timed out after {timeout}s.')
715
+ except Exception as e:
716
+ return (False, str(e))
717
+ finally:
718
+ try:
719
+ os.unlink(script_path)
720
+ except OSError:
721
+ pass
722
+
723
+ def _run_designer_react(task: str, system_prompt: str, model: str, api_key: str, base_url: str, temperature: float=0.9) -> str:
724
+ if _ReactAgent is None:
725
+ raise ImportError('ReactAgent not available')
726
+ os.environ['OPENAI_API_KEY'] = api_key
727
+ os.environ['OPENAI_BASE_URL'] = base_url
728
+ full_task = f'{system_prompt}\n\n{task}' if system_prompt and system_prompt.strip() else task
729
+ try:
730
+ agent = _ReactAgent(model_name=model, temperature=temperature)
731
+ except TypeError:
732
+ agent = _ReactAgent(model_name=model)
733
+ return _str_from_result(agent.run(full_task))
734
+
735
+ def _run_swarms_cloud_agent(task: str, system_prompt: str, model: str, api_key: str, temperature: float=0.9) -> str:
736
+ if _SwarmsClient is None:
737
+ raise ImportError('swarms_client not installed')
738
+ if not (api_key or '').strip():
739
+ raise ValueError('SWARMS_API_KEY required')
740
+ result = _SwarmsClient(api_key=api_key).agent.run(agent_config={'agent_name': 'LUAF Designer', 'description': 'Designs tokenized agents.', 'system_prompt': system_prompt, 'model_name': model, 'max_loops': DESIGNER_MAX_LOOPS, 'max_tokens': 8192, 'temperature': temperature}, task=task)
741
+ if not isinstance(result, dict):
742
+ return str(result).strip()
743
+ parts = []
744
+ for o in result.get('outputs') or []:
745
+ if isinstance(o, dict) and 'content' in o:
746
+ parts.append(str(o['content']).strip())
747
+ elif isinstance(o, str):
748
+ parts.append(o.strip())
749
+ return '\n'.join(parts).strip() or str(result).strip()
750
+
751
+ def _run_swarms_agent(prompt: str, model: str, api_key: str, base_url: str, temperature: float=0.9) -> str:
752
+ if SwarmsAgent is None:
753
+ raise ImportError('swarms not installed')
754
+ os.environ['OPENAI_API_KEY'] = api_key
755
+ os.environ['OPENAI_BASE_URL'] = base_url
756
+ return _str_from_result(SwarmsAgent(agent_name='LUAF Designer', agent_description='Designs tokenized agents.', model_name=model, max_loops=1, temperature=temperature).run(prompt))
757
+
758
+ _TOPIC_GEN_SYSTEM = 'You output exactly one sentence: the business idea. No greeting, no preamble, no "Certainly", "Here is", "Sure", or any other conversational lead-in. No bullet points, no explanation, no outline. Reply with only that single sentence.'
759
+
760
+ def _strip_topic_preamble(raw: str) -> str:
761
+ """Remove conversational preambles so only the one-sentence idea remains."""
762
+ s = (raw or '').strip()
763
+ if not s:
764
+ return s
765
+ s = re.sub(r'^(Certainly!?|Sure!?|Of course!?|Absolutely!?)\s*', '', s, flags=re.IGNORECASE)
766
+ s = re.sub(r'^Here\'?s?\s+(?:a\s+)?(?:full\s+)?(?:brief\s+)?(?:outline\s+)?(?:for\s+)?', '', s, flags=re.IGNORECASE)
767
+ s = re.sub(r'^Here\s+is\s+(?:a\s+)?(?:full\s+)?(?:outline\s+)?(?:for\s+)?', '', s, flags=re.IGNORECASE)
768
+ if '---' in s:
769
+ parts = s.split('---', 1)
770
+ if len(parts) > 1 and parts[1].strip():
771
+ s = parts[1].strip()
772
+ first_sentence = s.split('.')[0].strip()
773
+ if first_sentence and len(first_sentence) >= 20:
774
+ return (first_sentence + '.').strip()
775
+ return s.strip()[:500]
776
+
777
+ def _generate_topic_via_llm(api_key: str, base_url: str, model: str=LLM_MODEL) -> str:
778
+ if not (api_key or '').strip() or not (base_url or '').strip():
779
+ return ''
780
+ profile = get_active_profile()
781
+ prompt = (profile.get('topic_prompt') or _DEFAULT_TOPIC_PROMPT).strip()
782
+ try:
783
+ resp = requests.post(f"{base_url.rstrip('/')}/chat/completions", headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, json={'model': model, 'messages': [{'role': 'system', 'content': _TOPIC_GEN_SYSTEM}, {'role': 'user', 'content': prompt}], 'max_tokens': 120, 'temperature': 0.5}, timeout=min(60, LLM_HTTP_TIMEOUT))
784
+ if not resp.ok:
785
+ return ''
786
+ data = resp.json()
787
+ content = (data.get('choices') or [{}])[0].get('message', {}).get('content') or ''
788
+ content = _strip_topic_preamble(content)
789
+ return content[:500] or ''
790
+ except Exception as e:
791
+ logger.warning('Could not generate topic via LLM: {}', e)
792
+ return ''
793
+
794
+ def _find_latest_final_payload_in_workspace() -> Optional[str]:
795
+ root = Path(WORKSPACE_DIR)
796
+ if not root.exists():
797
+ return None
798
+ found: list[tuple[float, Path]] = []
799
+ for path in root.rglob(FINAL_PAYLOAD_FILENAME):
800
+ try:
801
+ if path.is_file():
802
+ found.append((path.stat().st_mtime, path))
803
+ except OSError:
804
+ pass
805
+ if not found:
806
+ return None
807
+ found.sort(key=lambda x: x[0], reverse=True)
808
+ try:
809
+ raw = found[0][1].read_text(encoding='utf-8').strip()
810
+ return raw if raw else None
811
+ except Exception:
812
+ return None
813
+
814
+ def _run_swarms_autonomous_agent(task: str, system_prompt: str, model: str, api_key: str, base_url: str, temperature: float=0.9) -> str:
815
+ if SwarmsAgent is None:
816
+ raise ImportError('swarms not installed')
817
+ os.environ['OPENAI_API_KEY'] = api_key
818
+ os.environ['OPENAI_BASE_URL'] = base_url
819
+ os.environ['WORKSPACE_DIR'] = WORKSPACE_DIR
820
+ agent = SwarmsAgent(agent_name='LUAF Designer', agent_description='Designs tokenized agents; saves JSON payload.', model_name=model, max_loops=DESIGNER_MAX_LOOPS, interactive=_env_bool('LUAF_INTERACTIVE', '0'), system_prompt=system_prompt, temperature=temperature, autosave=True, verbose=True, workspace_dir=WORKSPACE_DIR)
821
+ result_str = _str_from_result(agent.run(task))
822
+ ws_fn = getattr(agent, '_get_agent_workspace_dir', None)
823
+ try:
824
+ ws = ws_fn() if callable(ws_fn) else getattr(agent, 'workspace_dir', None)
825
+ except Exception:
826
+ ws = getattr(agent, 'workspace_dir', None)
827
+ if ws:
828
+ pp = Path(ws) / FINAL_PAYLOAD_FILENAME
829
+ if pp.exists():
830
+ try:
831
+ raw = pp.read_text(encoding='utf-8').strip()
832
+ if raw:
833
+ return raw
834
+ except Exception:
835
+ pass
836
+ from_workspace = _find_latest_final_payload_in_workspace()
837
+ if from_workspace:
838
+ return from_workspace
839
+ last = _extract_last_json_object(result_str)
840
+ return last if last else result_str
841
+
842
+ def _designer_subprocess_entry() -> None:
843
+ in_path = os.environ.get('DESIGNER_IN', '').strip()
844
+ out_path = os.environ.get('DESIGNER_OUT', '').strip()
845
+ if not in_path or not out_path:
846
+ return
847
+ try:
848
+ data = json.loads(Path(in_path).read_text(encoding='utf-8'))
849
+ raw = get_agent_payload_from_llm(topic=data['topic'], search_snippets=data.get('search_snippets', ''), model=data.get('model', LLM_MODEL), api_key=data.get('api_key', ''), base_url=data.get('base_url', 'https://api.openai.com/v1'), existing_names=data.get('existing_names'), existing_tickers=data.get('existing_tickers'), temperature=data.get('temperature'), validation_feedback=data.get('validation_feedback'), template_id=data.get('template_id'), retrieved_exemplars=data.get('retrieved_exemplars'), system_prompt_override=data.get('system_prompt'), product_focus_override=data.get('product_focus'))
850
+ Path(out_path).write_text(raw or '', encoding='utf-8')
851
+ except Exception as e:
852
+ Path(out_path).write_text(f'', encoding='utf-8')
853
+ logger.exception('Designer subprocess failed: {}', e)
854
+ raise
855
+
856
+ def _run_designer_in_subprocess(topic: str, search_snippets: str, model: str, api_key: str, base_url: str, existing_names: Optional[Iterable[str]]=None, existing_tickers: Optional[Iterable[str]]=None, temperature: Optional[float]=None, validation_feedback: Optional[str]=None, template_id: Optional[str]=None, retrieved_exemplars: Optional[list[str]]=None) -> str:
857
+ fd_in, path_in = tempfile.mkstemp(prefix='luaf_designer_in_', suffix='.json', text=True)
858
+ fd_out, path_out = tempfile.mkstemp(prefix='luaf_designer_out_', suffix='.txt', text=True)
859
+ try:
860
+ os.close(fd_out)
861
+ profile = get_active_profile()
862
+ payload = {'topic': topic, 'search_snippets': search_snippets, 'model': model, 'api_key': api_key, 'base_url': base_url, 'existing_names': list(existing_names) if existing_names is not None else [], 'existing_tickers': list(existing_tickers) if existing_tickers is not None else [], 'temperature': temperature, 'validation_feedback': validation_feedback, 'template_id': template_id, 'retrieved_exemplars': retrieved_exemplars if retrieved_exemplars is not None else [], 'system_prompt': profile.get('system_prompt'), 'product_focus': profile.get('product_focus')}
863
+ with os.fdopen(fd_in, 'w', encoding='utf-8') as f:
864
+ json.dump(payload, f, indent=0)
865
+ env = os.environ.copy()
866
+ env['DESIGNER_IN'] = path_in
867
+ env['DESIGNER_OUT'] = path_out
868
+ proc = subprocess.run([sys.executable, '-c', 'from LUAF import _designer_subprocess_entry; _designer_subprocess_entry()'], cwd=str(_LUAF_DIR), env=env, timeout=600)
869
+ if proc.returncode != 0:
870
+ raise RuntimeError(f'Designer subprocess exited with {proc.returncode}')
871
+ raw = Path(path_out).read_text(encoding='utf-8')
872
+ if not (raw or '').strip():
873
+ raise RuntimeError('Designer subprocess produced no output (empty DESIGNER_OUT file)')
874
+ return raw
875
+ finally:
876
+ try:
877
+ os.unlink(path_in)
878
+ except OSError:
879
+ pass
880
+ try:
881
+ os.unlink(path_out)
882
+ except OSError:
883
+ pass
884
+
885
+ def _build_designer_user_message(topic: str, search_snippets: str, existing_names: Optional[Iterable[str]], existing_tickers: Optional[Iterable[str]], validation_feedback: Optional[str], template: Any, retrieved_exemplars: Optional[list[str]]=None, product_focus_override: Optional[str]=None) -> str:
886
+ sections: list[str] = []
887
+ sections.append('## Topic\n' + (topic or '(none)'))
888
+ sections.append('\n' + _format_quality_packages_for_topic(topic or ''))
889
+ sections.append('\n## Search context (use to ground APIs, patterns, best practices)\n' + (search_snippets or '(none)'))
890
+ if retrieved_exemplars:
891
+ sections.append('\n## Similar agents / design context (use for inspiration only)\n' + '\n\n'.join(retrieved_exemplars))
892
+ if existing_names or existing_tickers:
893
+ constraints: list[str] = []
894
+ if existing_names:
895
+ constraints.append(f"Used names (do not reuse): {', '.join(sorted(set(existing_names)))}.")
896
+ if existing_tickers:
897
+ constraints.append(f"Used tickers (do not reuse): {', '.join(sorted(set(existing_tickers)))}.")
898
+ sections.append('\n## Constraints\n' + ' '.join(constraints))
899
+ seed = random.randint(10000, 99999)
900
+ angle = random.choice(DESIGN_ANGLES)
901
+ sections.append(f'\n## Design parameters\nSeed: {seed}. Angle: {angle}.')
902
+ required_keys = ', '.join(sorted(REQUIRED_PAYLOAD_KEYS))
903
+ product_focus = (product_focus_override or _DEFAULT_PRODUCT_FOCUS).strip()
904
+ sections.append(f"\n## Instructions\nFollow your process: Clarify → Architect → Implement → Describe. Write as an expert programmer: type hints, defensive code, real APIs, no dead code. Prefer external APIs that do not require API keys; when a key is required, use os.environ.get and add comments indicating where to obtain the key and where to set it (e.g. in .env). Do not use mock data, example data, or hardcoded fake responses; when real data requires credentials or external input, implement the real code path, read secrets from the environment, and add comments stating where to obtain and where to set each value (e.g. in .env). The agent's task must be executable to generate profit or measurable value; design for real-world utility. {product_focus} Output ONLY the single JSON object. No other text, no reasoning, no markdown. Use the swarms Agent; no stubs or placeholders; full production-quality code. The agent code must be at least 300 substantive lines (400+ preferred); no boilerplate or examples—only fully functioning runnable code. The script is validated by running it with no arguments (python script.py). Make all CLI arguments optional (e.g. argparse: use default=, never required=True).\n**Required top-level keys (exactly these, no others): {required_keys}. agent = full Python code string; useCases = array of {{{{title, description}}}}; requirements = array of {{{{package, installation}}}}; is_free = true.")
905
+ if validation_feedback and validation_feedback.strip():
906
+ sections.append('\n## Previous validation failure (fix before re-outputting)\nAddress every line of the validation error below. Fix all reported issues (imports, syntax, runtime). Then output only the corrected JSON object with no other text.\n\n' + validation_feedback.strip())
907
+ if template:
908
+ if getattr(template, 'usage_instructions', None):
909
+ sections.append('\n## Template usage\n' + template.usage_instructions)
910
+ if getattr(template, 'code_skeleton', None):
911
+ sections.append('\n## Code skeleton (expand into full implementation)\n\n' + template.code_skeleton)
912
+ return '\n'.join(sections).strip()
913
+
914
+ def get_agent_payload_from_llm(topic: str, search_snippets: str, model: str, api_key: str, base_url: str, use_swarms_agent: bool=True, existing_names: Optional[Iterable[str]]=None, existing_tickers: Optional[Iterable[str]]=None, temperature: Optional[float]=None, validation_feedback: Optional[str]=None, template_id: Optional[str]=None, retrieved_exemplars: Optional[list[str]]=None, system_prompt_override: Optional[str]=None, product_focus_override: Optional[str]=None) -> str:
915
+ if temperature is None:
916
+ temperature = LLM_TEMPERATURE
917
+ template = None
918
+ if template_id and (template_id := template_id.strip()) and (_get_template is not None):
919
+ template = _get_template(template_id)
920
+ if template:
921
+ logger.info('Using template: {}', template_id)
922
+ base_system = (system_prompt_override or get_active_profile().get('system_prompt') or DESIGNER_SYSTEM_PROMPT).strip()
923
+ if 'Required top-level keys' not in base_system or 'is_free (boolean true only)' not in base_system:
924
+ base_system = base_system + '\n\n' + PUBLICATION_OUTPUT_FORMAT_FRAGMENT
925
+ system = base_system + '\n\nSWARMS REF:' + SWARMS_AGENT_DOCS
926
+ if template and getattr(template, 'system_fragment', None):
927
+ system += '\n\n' + (template.system_fragment or '')
928
+ user = _build_designer_user_message(topic=topic, search_snippets=search_snippets, existing_names=existing_names, existing_tickers=existing_tickers, validation_feedback=validation_feedback, template=template, retrieved_exemplars=retrieved_exemplars, product_focus_override=product_focus_override or get_active_profile().get('product_focus'))
929
+ task_auto = user + f'\n\nSave final JSON to {FINAL_PAYLOAD_FILENAME} via create_file. Then complete_task.'
930
+ if DESIGNER_USE_DIRECT_API and (api_key or '').strip() and (base_url or '').strip():
931
+ try:
932
+ resp = requests.post(f"{base_url.rstrip('/')}/chat/completions", headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, json={'model': model, 'messages': [{'role': 'system', 'content': system}, {'role': 'user', 'content': user}], 'temperature': temperature, 'max_tokens': 8192}, timeout=LLM_HTTP_TIMEOUT)
933
+ if resp.ok:
934
+ choices = resp.json().get('choices')
935
+ if choices:
936
+ content = (choices[0].get('message', {}).get('content') or '').strip()
937
+ if content:
938
+ logger.info('Designer: used direct API (one-shot)')
939
+ return content
940
+ except Exception as e:
941
+ logger.warning('Designer direct API failed ({}), trying Swarms Agent', e)
942
+ if use_swarms_agent:
943
+ _try_fns = []
944
+ if DESIGNER_AGENT_ARCHITECTURE == 'react':
945
+ if _ReactAgent is not None:
946
+ _try_fns.append(('ReAct', lambda: _run_designer_react(task_auto, system, model, api_key, base_url, temperature)))
947
+ _try_fns.append(('Swarms Agent (ReAct fallback)', lambda: _run_swarms_autonomous_agent(task_auto, system, model, api_key, base_url, temperature)))
948
+ else:
949
+ sk = os.environ.get('SWARMS_API_KEY', '').strip()
950
+ cloud_fn = ('Swarms Cloud', lambda: _run_swarms_cloud_agent(task_auto, system, model, sk, temperature)) if sk and _SwarmsClient is not None else None
951
+ agent_fn = ('Swarms Agent', lambda: _run_swarms_autonomous_agent(task_auto, system, model, api_key, base_url, temperature))
952
+ if cloud_fn and _env_bool('LUAF_TRY_SWARMS_CLOUD_FIRST', '0'):
953
+ _try_fns.extend([cloud_fn, agent_fn])
954
+ else:
955
+ _try_fns.append(agent_fn)
956
+ if cloud_fn:
957
+ _try_fns.append(cloud_fn)
958
+ for label, fn in _try_fns:
959
+ try:
960
+ logger.info('Using {} for LLM call', label)
961
+ return fn()
962
+ except Exception as e:
963
+ logger.warning('{} failed ({}), trying next', label, e)
964
+ logger.info('Using direct OpenAI-compatible API')
965
+ resp = requests.post(f"{base_url.rstrip('/')}/chat/completions", headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, json={'model': model, 'messages': [{'role': 'system', 'content': system}, {'role': 'user', 'content': user}], 'temperature': temperature}, timeout=LLM_HTTP_TIMEOUT)
966
+ if not resp.ok:
967
+ raise RuntimeError(f'LLM failed: {resp.status_code} {resp.text[:500]}')
968
+ choices = resp.json().get('choices')
969
+ if not choices:
970
+ raise RuntimeError('LLM response had no choices')
971
+ return (choices[0].get('message', {}).get('content') or '').strip()
972
+
973
+ def _extract_json_object_spans(text: str) -> list[tuple[int, int]]:
974
+ if not text:
975
+ return []
976
+ spans, depth, start, i, in_str, esc, qc, n = ([], 0, None, 0, False, False, None, len(text))
977
+ while i < n:
978
+ c = text[i]
979
+ if esc:
980
+ esc = False
981
+ i += 1
982
+ continue
983
+ if c == '\\' and in_str:
984
+ esc = True
985
+ i += 1
986
+ continue
987
+ if c in ('"', "'") and (not esc):
988
+ if not in_str:
989
+ in_str, qc = (True, c)
990
+ elif c == qc:
991
+ in_str, qc = (False, None)
992
+ i += 1
993
+ continue
994
+ if not in_str:
995
+ if c == '{':
996
+ if depth == 0:
997
+ start = i
998
+ depth += 1
999
+ elif c == '}' and depth > 0:
1000
+ depth -= 1
1001
+ if depth == 0 and start is not None:
1002
+ spans.append((start, i + 1))
1003
+ i += 1
1004
+ return spans
1005
+
1006
+ _extract_json_object_spans_DEL = _extract_json_object_spans # legacy alias
1007
+
1008
+ def _extract_first_json_object(text: str) -> str:
1009
+ s = _extract_json_object_spans(text)
1010
+ return text[s[0][0]:s[0][1]] if s else ''
1011
+
1012
+ def _extract_last_json_object(text: str) -> str:
1013
+ s = _extract_json_object_spans(text)
1014
+ return text[s[-1][0]:s[-1][1]] if s else ''
1015
+
1016
+ DESIGNER_EXEMPLARS_PATH = _LUAF_DIR / 'designer_exemplars.jsonl'
1017
+ _exemplar_cache: Optional[list[tuple[list[float], str]]] = None
1018
+
1019
+ def _cosine_similarity(a: list[float], b: list[float]) -> float:
1020
+ if not a or not b or len(a) != len(b):
1021
+ return 0.0
1022
+ dot = sum((x * y for x, y in zip(a, b)))
1023
+ na = sum((x * x for x in a)) ** 0.5
1024
+ nb = sum((x * x for x in b)) ** 0.5
1025
+ if na == 0 or nb == 0:
1026
+ return 0.0
1027
+ return dot / (na * nb)
1028
+
1029
+ def _get_query_embedding(query: str) -> Optional[list[float]]:
1030
+ if not (query or '').strip():
1031
+ return None
1032
+ try:
1033
+ from sentence_transformers import SentenceTransformer
1034
+ model = SentenceTransformer('all-MiniLM-L6-v2')
1035
+ emb = model.encode([query.strip()], convert_to_numpy=False)
1036
+ vec = emb[0]
1037
+ return vec.tolist() if hasattr(vec, 'tolist') else list(vec)
1038
+ except Exception:
1039
+ api_key = os.environ.get('OPENAI_API_KEY', '').strip()
1040
+ base_url = (os.environ.get('OPENAI_BASE_URL') or 'https://api.openai.com/v1').strip()
1041
+ if not api_key:
1042
+ return None
1043
+ try:
1044
+ resp = requests.post(f"{base_url.rstrip('/')}/embeddings", headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, json={'model': 'text-embedding-3-small', 'input': [query.strip()]}, timeout=30)
1045
+ if not resp.ok or not resp.json().get('data'):
1046
+ return None
1047
+ return resp.json()['data'][0]['embedding']
1048
+ except Exception:
1049
+ return None
1050
+
1051
+ def _embed_many(texts: list[str]) -> Optional[list[list[float]]]:
1052
+ if not texts:
1053
+ return []
1054
+ texts = [t.strip() if (t or '').strip() else '' for t in texts]
1055
+ try:
1056
+ from sentence_transformers import SentenceTransformer
1057
+ model = SentenceTransformer('all-MiniLM-L6-v2')
1058
+ emb = model.encode(texts, convert_to_numpy=False)
1059
+ return [e.tolist() if hasattr(e, 'tolist') else list(e) for e in emb]
1060
+ except Exception:
1061
+ api_key = os.environ.get('OPENAI_API_KEY', '').strip()
1062
+ base_url = (os.environ.get('OPENAI_BASE_URL') or 'https://api.openai.com/v1').strip()
1063
+ if not api_key:
1064
+ return None
1065
+ try:
1066
+ resp = requests.post(f"{base_url.rstrip('/')}/embeddings", headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, json={'model': 'text-embedding-3-small', 'input': texts}, timeout=60)
1067
+ if not resp.ok:
1068
+ return None
1069
+ data = resp.json().get('data') or []
1070
+ return [item['embedding'] for item in data]
1071
+ except Exception:
1072
+ return None
1073
+
1074
+ def _multihop_web_rag(topic: str, max_hops: int=3, threshold: float=0.7, total_k: int=20, ddg_per_hop: int=15) -> str:
1075
+ topic = (topic or '').strip()[:500]
1076
+ if not topic:
1077
+ return ''
1078
+ all_snippets: list[str] = []
1079
+ query = topic
1080
+ topic_vec: Optional[list[float]] = _get_query_embedding(topic)
1081
+ for hop in range(max_hops):
1082
+ snippets_this_hop: list[str] = []
1083
+ for attempt in range(2):
1084
+ snippets_this_hop = _search_duckduckgo_snippets_list(query, ddg_per_hop)
1085
+ if snippets_this_hop:
1086
+ break
1087
+ if not snippets_this_hop:
1088
+ break
1089
+ all_snippets.extend(snippets_this_hop)
1090
+ if hop + 1 >= max_hops:
1091
+ break
1092
+ if topic_vec is not None:
1093
+ new_embs = _embed_many(snippets_this_hop)
1094
+ if new_embs:
1095
+ scores = [_cosine_similarity(emb, topic_vec) for emb in new_embs]
1096
+ if max(scores) < threshold:
1097
+ break
1098
+ refined_parts = [topic]
1099
+ for s in snippets_this_hop[:3]:
1100
+ refined_parts.append((s or '')[:200].strip())
1101
+ query = ' '.join(refined_parts)[:500]
1102
+ if not all_snippets:
1103
+ return ''
1104
+ seen: set[str] = set()
1105
+ deduped: list[str] = []
1106
+ for s in all_snippets:
1107
+ key = re.sub('\\s+', ' ', (s or '').strip().lower())[:200]
1108
+ if key and key not in seen:
1109
+ seen.add(key)
1110
+ deduped.append((s or '').strip())
1111
+ if not deduped:
1112
+ return ''
1113
+ if topic_vec is not None:
1114
+ snippet_embs = _embed_many(deduped)
1115
+ if snippet_embs and len(snippet_embs) == len(deduped):
1116
+ scored = [(_cosine_similarity(emb, topic_vec), s) for emb, s in zip(snippet_embs, deduped)]
1117
+ scored.sort(key=lambda x: -x[0])
1118
+ deduped = [s for _, s in scored[:total_k]]
1119
+ else:
1120
+ deduped = deduped[:total_k]
1121
+ else:
1122
+ deduped = deduped[:total_k]
1123
+ return '\n'.join(deduped)
1124
+
1125
+ def _retrieve_similar_exemplars(topic: str, search_snippets: str, top_k: int=3) -> list[str]:
1126
+ if (os.environ.get('LUAF_USE_RETRIEVAL', '1') or '').strip().lower() in ('0', 'false', 'no'):
1127
+ return []
1128
+ path = DESIGNER_EXEMPLARS_PATH
1129
+ if not path.exists():
1130
+ return []
1131
+ global _exemplar_cache
1132
+ try:
1133
+ exemplars_raw: list[dict[str, Any]] = []
1134
+ with open(path, encoding='utf-8', errors='replace') as f:
1135
+ for line in f:
1136
+ line = line.strip()
1137
+ if not line:
1138
+ continue
1139
+ try:
1140
+ exemplars_raw.append(json.loads(line))
1141
+ except json.JSONDecodeError:
1142
+ continue
1143
+ if not exemplars_raw:
1144
+ return []
1145
+ texts_to_embed = [e.get('text') or str(e) for e in exemplars_raw]
1146
+ query = f"topic: {(topic or '').strip()[:500]}\ncontext: {(search_snippets or '')[:500]}".strip()
1147
+ embeddings: list[list[float]]
1148
+ query_vec: list[float]
1149
+ if _exemplar_cache is None:
1150
+ try:
1151
+ from sentence_transformers import SentenceTransformer
1152
+ model = SentenceTransformer('all-MiniLM-L6-v2')
1153
+ embeddings = model.encode(texts_to_embed, convert_to_numpy=False)
1154
+ if hasattr(embeddings, 'tolist'):
1155
+ embeddings = [e.tolist() if hasattr(e, 'tolist') else list(e) for e in embeddings]
1156
+ else:
1157
+ embeddings = [list(e) for e in embeddings]
1158
+ query_emb = model.encode([query], convert_to_numpy=False)
1159
+ query_vec = query_emb[0].tolist() if hasattr(query_emb[0], 'tolist') else list(query_emb[0])
1160
+ except Exception:
1161
+ api_key = os.environ.get('OPENAI_API_KEY', '').strip()
1162
+ base_url = (os.environ.get('OPENAI_BASE_URL') or 'https://api.openai.com/v1').strip()
1163
+ if not api_key:
1164
+ return []
1165
+
1166
+ def _embed_openai(texts: list[str]) -> list[list[float]]:
1167
+ resp = requests.post(f"{base_url.rstrip('/')}/embeddings", headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, json={'model': 'text-embedding-3-small', 'input': texts}, timeout=30)
1168
+ if not resp.ok:
1169
+ raise RuntimeError(f'Embeddings API {resp.status_code}')
1170
+ data = resp.json()
1171
+ return [item['embedding'] for item in data.get('data', [])]
1172
+ embeddings = _embed_openai(texts_to_embed)
1173
+ query_vec = _embed_openai([query])[0]
1174
+ _exemplar_cache = list(zip(embeddings, texts_to_embed))
1175
+ else:
1176
+ try:
1177
+ from sentence_transformers import SentenceTransformer
1178
+ model = SentenceTransformer('all-MiniLM-L6-v2')
1179
+ query_emb = model.encode([query], convert_to_numpy=False)
1180
+ query_vec = query_emb[0].tolist() if hasattr(query_emb[0], 'tolist') else list(query_emb[0])
1181
+ except Exception:
1182
+ api_key = os.environ.get('OPENAI_API_KEY', '').strip()
1183
+ base_url = (os.environ.get('OPENAI_BASE_URL') or 'https://api.openai.com/v1').strip()
1184
+ query_vec = []
1185
+ if api_key:
1186
+ resp = requests.post(f"{base_url.rstrip('/')}/embeddings", headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, json={'model': 'text-embedding-3-small', 'input': [query]}, timeout=30)
1187
+ if resp.ok:
1188
+ data = resp.json()
1189
+ if data.get('data'):
1190
+ query_vec = data['data'][0]['embedding']
1191
+ if not query_vec:
1192
+ return []
1193
+ scored = [(_cosine_similarity(emb, query_vec), text) for emb, text in _exemplar_cache]
1194
+ scored.sort(key=lambda x: -x[0])
1195
+ return [text for _, text in scored[:top_k]]
1196
+ except Exception as e:
1197
+ logger.debug('Retrieval skipped: {}', e)
1198
+ return []
1199
+
1200
+ def _luaf_agent_dir() -> Path:
1201
+ return Path(__file__).resolve().parent
1202
+
1203
+ def _luaf_get_current_organism() -> dict[str, Any]:
1204
+ a = _luaf_agent_dir()
1205
+ pkl = a / 'planner_weights' / 'current.pkl'
1206
+ cp = a / 'population' / 'current' / 'config.json'
1207
+ if not cp.exists():
1208
+ cp = a / 'organism_config.json'
1209
+ cfg: dict[str, Any] = {}
1210
+ if cp.exists():
1211
+ try:
1212
+ cfg = json.loads(cp.read_text(encoding='utf-8'))
1213
+ except Exception:
1214
+ pass
1215
+ return {'planner_weights_path': str(pkl) if pkl.exists() else None, 'config': cfg, 'config_path': str(cp)}
1216
+
1217
+ def _luaf_set_current_organism(state: dict[str, Any]) -> None:
1218
+ a = _luaf_agent_dir()
1219
+ dst = a / 'planner_weights' / 'current.pkl'
1220
+ dst.parent.mkdir(parents=True, exist_ok=True)
1221
+ sw = state.get('planner_weights_path')
1222
+ if sw:
1223
+ src = Path(sw) if Path(sw).is_absolute() else a / sw
1224
+ if src.exists() and src != dst:
1225
+ try:
1226
+ import shutil
1227
+ shutil.copy2(src, dst)
1228
+ except Exception:
1229
+ pass
1230
+ cd = a / 'population' / 'current'
1231
+ cd.mkdir(parents=True, exist_ok=True)
1232
+ (cd / 'config.json').write_text(json.dumps(state.get('config') or {}, indent=2), encoding='utf-8')
1233
+
1234
+ def _luaf_should_evolve(force_disable: Optional[bool]=None, force_enable: Optional[bool]=None) -> bool:
1235
+ if force_disable or os.environ.get('LUAF_MUTATE_THIS_RUN', '').strip() == '0':
1236
+ return False
1237
+ if force_enable:
1238
+ return True
1239
+ return os.environ.get('LUAF_EVOLVE', '').strip() == '1'
1240
+
1241
+ def _luaf_add_noise(params: Any) -> Any:
1242
+ try:
1243
+ from jax import tree_map
1244
+ import numpy as np
1245
+ return tree_map(lambda x: x + np.float32(0.02 * np.random.randn(*x.shape)) if hasattr(x, 'shape') else x, params)
1246
+ except ImportError:
1247
+ try:
1248
+ import numpy as np
1249
+ if hasattr(params, 'keys'):
1250
+ return type(params)({k: _luaf_add_noise(v) for k, v in params.items()})
1251
+ return params + np.float32(0.02 * np.random.randn(*params.shape)) if hasattr(params, 'shape') else params
1252
+ except ImportError:
1253
+ return params
1254
+
1255
+ def _luaf_mutate_planner(state: dict[str, Any]) -> Optional[dict[str, Any]]:
1256
+ a = _luaf_agent_dir()
1257
+ wp = state.get('planner_weights_path')
1258
+ p = Path(wp) if wp else a / 'planner_weights' / 'current.pkl'
1259
+ if not p.is_absolute():
1260
+ p = a / p
1261
+ if not p.exists():
1262
+ return None
1263
+ try:
1264
+ with open(p, 'rb') as f:
1265
+ params = pickle.load(f)
1266
+ except Exception:
1267
+ return None
1268
+ cd = a / 'planner_weights' / 'candidates'
1269
+ cd.mkdir(parents=True, exist_ok=True)
1270
+ cid = uuid.uuid4().hex[:8]
1271
+ op = cd / f'{cid}.pkl'
1272
+ try:
1273
+ with open(op, 'wb') as f:
1274
+ pickle.dump(_luaf_add_noise(params), f)
1275
+ except Exception:
1276
+ return None
1277
+ return {**state, 'planner_weights_path': str(op)}
1278
+ _EVOLVE_TESTS = [('DeFi analytics', 'DeFi context'), ('Trading bot', 'Trading context'), ('Research summariser', 'Summarisation context')]
1279
+
1280
+ def _luaf_evaluate(state: dict[str, Any], timeout: int=60) -> tuple[bool, float]:
1281
+ if not (_plan_from_topic_and_search and _execute_plan and _get_template):
1282
+ return (False, 0.0)
1283
+ wp = state.get('planner_weights_path') or os.environ.get('LUAF_PLANNER_WEIGHTS')
1284
+ prev = os.environ.get('LUAF_PLANNER_WEIGHTS')
1285
+ if wp:
1286
+ os.environ['LUAF_PLANNER_WEIGHTS'] = str(wp)
1287
+ try:
1288
+ val, hashes = (1.0, [])
1289
+ for t, s in _EVOLVE_TESTS:
1290
+ try:
1291
+ plan = _plan_from_topic_and_search(t, s, use_model=bool(wp))
1292
+ pl = _execute_plan(plan, _get_template, required_payload_keys=REQUIRED_PAYLOAD_KEYS)
1293
+ except Exception:
1294
+ val = 0.0
1295
+ break
1296
+ hashes.append(hashlib.sha256(str(sorted(plan.items())).encode()).hexdigest()[:16])
1297
+ code = pl.get('agent', '') if isinstance(pl.get('agent'), str) else str(pl.get('agent', ''))
1298
+ if not run_agent_code_validation(code, timeout)[0]:
1299
+ val = 0.0
1300
+ div = len(set(hashes)) / max(1, len(hashes)) if hashes else 0.0
1301
+ return (val >= 1.0, val + 0.1 * div)
1302
+ finally:
1303
+ if wp:
1304
+ if prev is not None:
1305
+ os.environ['LUAF_PLANNER_WEIGHTS'] = prev
1306
+ else:
1307
+ os.environ.pop('LUAF_PLANNER_WEIGHTS', None)
1308
+
1309
+ def _luaf_run_evolution() -> tuple[dict[str, Any], bool]:
1310
+ st = _luaf_get_current_organism()
1311
+ c = _luaf_mutate_planner(st)
1312
+ if c is None:
1313
+ return (st, False)
1314
+ ok_c, sc_c = _luaf_evaluate(c)
1315
+ if not ok_c:
1316
+ return (st, False)
1317
+ ok_s, sc_s = _luaf_evaluate(st)
1318
+ if sc_c <= (sc_s if ok_s else 0.0):
1319
+ return (st, False)
1320
+ _luaf_set_current_organism(c)
1321
+ return (c, True)
1322
+
1323
+ def _luaf_run_self_train(topic: str, use_search: bool=True) -> bool:
1324
+ a = _luaf_agent_dir()
1325
+ if str(a) not in sys.path:
1326
+ sys.path.insert(0, str(a))
1327
+ topics = [t for t in [topic.strip(), 'DeFi analytics', 'Trading bot', 'Research summariser', 'On-chain metrics', 'Backtesting strategy', 'Real-time alerts', 'Multi-DEX comparison', 'Risk metrics'] if t]
1328
+ if not topics:
1329
+ return False
1330
+ sid = uuid.uuid4().hex[:8]
1331
+ dd = a / 'planner_data'
1332
+ dd.mkdir(parents=True, exist_ok=True)
1333
+ jp = dd / f'self_train_{sid}.jsonl'
1334
+ cd = a / 'planner_weights' / 'candidates'
1335
+ cd.mkdir(parents=True, exist_ok=True)
1336
+ cp = cd / f'{sid}.pkl'
1337
+ try:
1338
+ from planner.data_pipeline import run_pipeline
1339
+ except ImportError:
1340
+ logger.error('planner.data_pipeline unavailable')
1341
+ return False
1342
+ try:
1343
+ run_pipeline(topics, jp, use_search=use_search, max_search_results=10)
1344
+ except Exception as e:
1345
+ logger.warning('self_train pipeline: {}', e)
1346
+ return False
1347
+ try:
1348
+ subprocess.run([sys.executable, '-m', 'planner.train', '--data', str(jp), '--out', str(cp)], cwd=str(a), env=os.environ.copy(), check=True, timeout=600)
1349
+ except Exception as e:
1350
+ logger.warning('self_train train: {}', e)
1351
+ return False
1352
+ cur = _luaf_get_current_organism()
1353
+ cand = {**cur, 'planner_weights_path': str(cp)}
1354
+ ok_c, sc_c = _luaf_evaluate(cand)
1355
+ if not ok_c:
1356
+ return False
1357
+ ok_s, sc_s = _luaf_evaluate(cur)
1358
+ if sc_c <= (sc_s if ok_s else 0.0):
1359
+ return False
1360
+ _luaf_set_current_organism(cand)
1361
+ logger.info('self_train: adopted candidate')
1362
+ return True
1363
+
1364
+ def _run_evolution_standalone() -> None:
1365
+ try:
1366
+ _, u = _luaf_run_evolution()
1367
+ logger.info('Evolution done. Updated: {}', u)
1368
+ except Exception as e:
1369
+ logger.warning('Evolution failed: {}', e)
1370
+
1371
+ def _schedule_x_post_for_agent(payload: dict[str, Any], res: dict[str, Any]) -> None:
1372
+ """If X post is enabled, add agent to pending and maybe post a batch. Best-effort; never raises."""
1373
+ if _add_agent_to_x_pending is None or _maybe_post_x_batch is None or _is_x_post_enabled is None or not _is_x_post_enabled():
1374
+ return
1375
+ try:
1376
+ _add_agent_to_x_pending(payload, res)
1377
+ _maybe_post_x_batch()
1378
+ except Exception as e:
1379
+ logger.warning('X post scheduling failed: {}', e)
1380
+
1381
+ def _drain_x_queue_if_enabled() -> None:
1382
+ """Drain X post queue if the X layer is available and enabled. Best-effort; never raises."""
1383
+ if _drain_x_queue is None or _is_x_post_enabled is None or not _is_x_post_enabled():
1384
+ logger.debug('X queue drain skipped (disabled or module not loaded).')
1385
+ return
1386
+ try:
1387
+ _drain_x_queue()
1388
+ except Exception as e:
1389
+ logger.warning('X queue drain failed: {}', e)
1390
+
1391
+ def _run_social_standalone() -> None:
1392
+ if _run_social_autonomy is None:
1393
+ logger.error('OpenClaw not available.')
1394
+ return
1395
+ try:
1396
+ b = input(f'Brief (Enter={TOPIC}): ').strip() or TOPIC
1397
+ r = _run_social_autonomy(b)
1398
+ logger.info('Social: {}', 'sent' if r else 'no action')
1399
+ except EOFError:
1400
+ _run_social_autonomy(TOPIC)
1401
+ except Exception as e:
1402
+ logger.warning('Social failed: {}', e)
1403
+
1404
+ def _run_self_train_standalone() -> None:
1405
+ try:
1406
+ t = input(f'Topic (Enter={TOPIC}): ').strip() or TOPIC
1407
+ _luaf_run_self_train(t)
1408
+ except EOFError:
1409
+ _luaf_run_self_train(TOPIC)
1410
+ except Exception as e:
1411
+ logger.warning('Self-train failed: {}', e)
1412
+
1413
+ def _run_build_dataset_standalone() -> None:
1414
+ try:
1415
+ from planner.data_pipeline import run_pipeline
1416
+ except ImportError:
1417
+ logger.error('planner.data_pipeline unavailable')
1418
+ return
1419
+ try:
1420
+ ts = input('Topics (comma/path, Enter=TOPIC): ').strip() or TOPIC
1421
+ if ts.endswith('.txt'):
1422
+ with open(ts, encoding='utf-8') as f:
1423
+ topics = [l.strip() for l in f if l.strip()]
1424
+ else:
1425
+ topics = [t.strip() for t in ts.split(',') if t.strip()]
1426
+ if not topics:
1427
+ logger.warning('No topics.')
1428
+ return
1429
+ op = _luaf_agent_dir() / 'planner_data' / 'train.jsonl'
1430
+ op.parent.mkdir(parents=True, exist_ok=True)
1431
+ run_pipeline(topics, op, use_search=True)
1432
+ logger.info('Wrote {}', op)
1433
+ except EOFError:
1434
+ run_pipeline([TOPIC], _luaf_agent_dir() / 'planner_data' / 'train.jsonl', use_search=True)
1435
+ except Exception as e:
1436
+ logger.warning('Build dataset failed: {}', e)
1437
+
1438
+ def _run_train_planner_standalone() -> None:
1439
+ ad = _luaf_agent_dir()
1440
+ dd = ad / 'planner_data' / 'train.jsonl'
1441
+ do = ad / 'planner_weights' / 'current.pkl'
1442
+ try:
1443
+ ds = input(f'Data ({dd}): ').strip() or str(dd)
1444
+ os_ = input(f'Out ({do}): ').strip() or str(do)
1445
+ except EOFError:
1446
+ ds, os_ = (str(dd), str(do))
1447
+ if not Path(ds).exists():
1448
+ logger.error('Not found: {}', ds)
1449
+ return
1450
+ try:
1451
+ subprocess.run([sys.executable, '-m', 'planner.train', '--data', ds, '--out', os_], cwd=str(ad), check=True)
1452
+ logger.info('Saved {}', os_)
1453
+ except Exception as e:
1454
+ logger.warning('Train failed: {}', e)
1455
+ _log_queue: 'queue.Queue[str]' = queue.Queue()
1456
+ _tui_stop_requested: bool = False
1457
+ _tui_current_topic: str = ''
1458
+ _tui_session_published: int = 0
1459
+ _tui_session_last_name: str = ''
1460
+ _tui_stopped_reason: str = ''
1461
+
1462
+ def _run_pipeline_with_brief(brief: str) -> None:
1463
+ os.environ['LUAF_DESIGN_BRIEF'] = (brief or TOPIC).strip()
1464
+ main()
1465
+
1466
+ def _get_next_persistent_topic(state: list[int]) -> str:
1467
+ if PERSISTENT_TOPIC_SOURCE == 'single':
1468
+ suffix = f' {random.choice(SEARCH_VARIANT_SUFFIXES)}' if random.random() < 0.5 else ''
1469
+ return (TOPIC + suffix).strip()
1470
+ if PERSISTENT_TOPIC_SOURCE == 'env':
1471
+ raw = (os.environ.get('LUAF_TOPIC_LIST') or '').strip()
1472
+ topics = [t.strip() for t in raw.split(',') if t.strip()]
1473
+ if not topics:
1474
+ return TOPIC
1475
+ idx = state[0] % len(topics)
1476
+ state[0] += 1
1477
+ return topics[idx]
1478
+ if PERSISTENT_TOPIC_SOURCE == 'file':
1479
+ path_str = (os.environ.get('LUAF_TOPIC_FILE') or '').strip()
1480
+ if not path_str:
1481
+ return TOPIC
1482
+ p = Path(path_str)
1483
+ if not p.exists():
1484
+ logger.warning('LUAF_TOPIC_FILE not found: {}', path_str)
1485
+ return TOPIC
1486
+ try:
1487
+ lines = [ln.strip() for ln in p.read_text(encoding='utf-8').splitlines() if ln.strip()]
1488
+ except Exception as e:
1489
+ logger.warning('Could not read LUAF_TOPIC_FILE: {}', e)
1490
+ return TOPIC
1491
+ if not lines:
1492
+ return TOPIC
1493
+ idx = state[0] % len(lines)
1494
+ state[0] += 1
1495
+ return lines[idx]
1496
+ return TOPIC
1497
+
1498
+ def run_persistent() -> None:
1499
+ global _tui_current_topic, _tui_session_published, _tui_session_last_name, _tui_stopped_reason
1500
+ api_key = os.environ.get('OPENAI_API_KEY', '').strip()
1501
+ base_url = (os.environ.get('OPENAI_BASE_URL') or 'https://api.openai.com/v1').strip()
1502
+ swarms_key = (os.environ.get('SWARMS_API_KEY') or SWARMS_API_KEY_FALLBACK or '').strip()
1503
+ if not api_key:
1504
+ logger.error('OPENAI_API_KEY not set')
1505
+ return
1506
+ pkey = get_private_key_from_env()
1507
+ cwallet = (os.environ.get('SOLANA_PUBKEY') or os.environ.get('CREATOR_WALLET') or '').strip()
1508
+ pubkey = get_creator_pubkey()
1509
+ run_task_override = (os.environ.get(PERSISTENT_RUN_TASK_ENV) or '').strip()
1510
+ topic_state: list[int] = [0]
1511
+ used_n: set[str] = set()
1512
+ used_t: set[str] = set()
1513
+ vfb: Optional[str] = None
1514
+ tmpl = (os.environ.get('LUAF_TEMPLATE') or '').strip() or None
1515
+ _drain_x_queue_if_enabled()
1516
+ while True:
1517
+ if _tui_stop_requested:
1518
+ _tui_stopped_reason = 'stop'
1519
+ logger.info('Stop requested; exiting persistent loop.')
1520
+ return
1521
+ balance = get_solana_balance(pubkey, SOLANA_RPC_URL)
1522
+ logger.info('Persistent: balance={:.4f} SOL, target={} SOL', balance, PERSISTENT_TARGET_SOL)
1523
+ if balance >= PERSISTENT_TARGET_SOL:
1524
+ _tui_stopped_reason = 'target'
1525
+ logger.info('Target SOL reached ({} >= {}). Exiting.', balance, PERSISTENT_TARGET_SOL)
1526
+ return
1527
+ brief = _get_next_persistent_topic(topic_state)
1528
+ if not (brief or '').strip():
1529
+ brief = _generate_topic_via_llm(api_key, base_url) or _DEFAULT_TOPIC
1530
+ logger.info('Brief was blank; generated: {}', brief[:200] + '...' if len(brief) > 200 else brief)
1531
+ _tui_current_topic = (brief or '')[:60].strip() or '—'
1532
+ logger.info('Brief: {}', brief[:200] + '...' if len(brief) > 200 else brief)
1533
+ snip: str
1534
+ if USE_MULTIHOP_WEB_RAG:
1535
+ snip = _multihop_web_rag(brief, max_hops=RAG_MAX_HOPS, threshold=RAG_CONVERGE_THRESHOLD, total_k=RAG_TOTAL_K, ddg_per_hop=RAG_DDG_PER_HOP)
1536
+ if not snip:
1537
+ snip = search_duckduckgo(f'{brief} {random.choice(SEARCH_VARIANT_SUFFIXES)}', max_results=DUCKDUCKGO_MAX_RESULTS)
1538
+ else:
1539
+ snip = search_duckduckgo(f'{brief} {random.choice(SEARCH_VARIANT_SUFFIXES)}', max_results=DUCKDUCKGO_MAX_RESULTS)
1540
+ snip = _append_keyless_api_search(brief, snip)
1541
+ payload: Optional[dict[str, Any]] = None
1542
+ if USE_PLANNER and _plan_from_topic_and_search and _execute_plan and _get_template:
1543
+ try:
1544
+ plan = _plan_from_topic_and_search(brief, snip, use_model=True)
1545
+ payload = _execute_plan(plan, _get_template, required_payload_keys=REQUIRED_PAYLOAD_KEYS)
1546
+ code = str(payload.get('agent') or '')
1547
+ if _is_skeleton_agent_code(code) and USE_DESIGNER:
1548
+ tmpl = (plan.get('template_id') or '').strip() or tmpl
1549
+ pname, pticker = (plan.get('name') or '', plan.get('ticker') or '')
1550
+ if pname or pticker:
1551
+ brief = f'{brief}\n(Preferred: name={pname}, ticker={pticker})'
1552
+ payload = None
1553
+ except Exception as e:
1554
+ logger.warning('Planner failed: {}', e)
1555
+ payload = None
1556
+ if payload is None and USE_DESIGNER:
1557
+ try:
1558
+ retrieved = _retrieve_similar_exemplars(brief, snip, top_k=3)
1559
+ if (os.environ.get('LUAF_DESIGNER_SUBPROCESS', '1') or '').strip().lower() not in ('0', 'false', 'no'):
1560
+ raw = _run_designer_in_subprocess(topic=brief, search_snippets=snip, model=LLM_MODEL, api_key=api_key, base_url=base_url, existing_names=used_n, existing_tickers=used_t, temperature=LLM_TEMPERATURE, validation_feedback=vfb, template_id=tmpl, retrieved_exemplars=retrieved)
1561
+ else:
1562
+ raw = get_agent_payload_from_llm(topic=brief, search_snippets=snip, model=LLM_MODEL, api_key=api_key, base_url=base_url, existing_names=used_n, existing_tickers=used_t, temperature=LLM_TEMPERATURE, validation_feedback=vfb, template_id=tmpl, retrieved_exemplars=retrieved)
1563
+ except Exception as e:
1564
+ logger.error('LLM failed: {}', e)
1565
+ vfb = f'LLM: {e!s}'
1566
+ if PERSISTENT_LOOP_SLEEP_SECONDS > 0:
1567
+ time.sleep(PERSISTENT_LOOP_SLEEP_SECONDS)
1568
+ continue
1569
+ try:
1570
+ payload = parse_agent_payload(raw)
1571
+ except ValueError as e:
1572
+ logger.error('Parse: {}', e)
1573
+ vfb = f'Parse: {e!s}'
1574
+ if PERSISTENT_LOOP_SLEEP_SECONDS > 0:
1575
+ time.sleep(PERSISTENT_LOOP_SLEEP_SECONDS)
1576
+ continue
1577
+ if payload is None:
1578
+ logger.warning('No payload; skipping iteration.')
1579
+ if PERSISTENT_LOOP_SLEEP_SECONDS > 0:
1580
+ time.sleep(PERSISTENT_LOOP_SLEEP_SECONDS)
1581
+ continue
1582
+ code = str(payload.get('agent') or '')
1583
+ sk_fb = _skeleton_validation_feedback(code)
1584
+ if sk_fb is not None:
1585
+ vfb = sk_fb
1586
+ logger.warning('Unit code too short or skeleton: {}', sk_fb[:200])
1587
+ if PERSISTENT_LOOP_SLEEP_SECONDS > 0:
1588
+ time.sleep(PERSISTENT_LOOP_SLEEP_SECONDS)
1589
+ continue
1590
+ ok, fb = run_agent_code_validation(code, VALIDATION_TIMEOUT)
1591
+ if not ok:
1592
+ vfb = fb
1593
+ logger.warning('Unit validation failed: {}', fb[:1500])
1594
+ logger.info('Validation full feedback:\n{}', fb[:3000] + ('...' if len(fb) > 3000 else ''))
1595
+ if _ask_publish_without_validation():
1596
+ logger.info('Publishing without validation (user confirmed).')
1597
+ else:
1598
+ if PERSISTENT_LOOP_SLEEP_SECONDS > 0:
1599
+ time.sleep(PERSISTENT_LOOP_SLEEP_SECONDS)
1600
+ continue
1601
+ n, t = ((payload.get('name') or '').strip(), (payload.get('ticker') or '').strip())
1602
+ used_n.add(n.lower())
1603
+ used_t.add(t.upper())
1604
+ dry_run_this = DRY_RUN
1605
+ if not DRY_RUN:
1606
+ bal = get_solana_balance(pubkey, SOLANA_RPC_URL)
1607
+ if bal < PERSISTENT_MIN_SOL_TO_TOKENIZE:
1608
+ dry_run_this = True
1609
+ logger.info('Insufficient balance ({:.4f} < {}); dry-run publish.', bal, PERSISTENT_MIN_SOL_TO_TOKENIZE)
1610
+ res = publish_agent(payload, swarms_key, pkey or '', dry_run_this, creator_wallet=cwallet)
1611
+ run_task = run_task_override or brief
1612
+ run_ok, run_out = run_agent_once(code, run_task, timeout=VALIDATION_TIMEOUT)
1613
+ if run_ok:
1614
+ logger.info('Run unit once: OK. Output length={}', len(run_out or ''))
1615
+ else:
1616
+ logger.warning('Run unit once: {}', run_out[:300] if run_out else 'failed')
1617
+ if USE_RUN_IN_NEW_TERMINAL:
1618
+ saved_path = _save_generated_agent(code, n, t, 0)
1619
+ if saved_path:
1620
+ run_agent_in_new_terminal(saved_path, (run_task or '').strip() or 'Run a quick check.')
1621
+ if res and (not dry_run_this):
1622
+ lu, rid, ca = (res.get('listing_url'), res.get('id'), res.get('token_address'))
1623
+ if lu or rid or ca:
1624
+ published_at = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
1625
+ append_agent_to_registry(AGENTS_REGISTRY_PATH, name=n, ticker=t, listing_url=lu, id_=rid, token_address=ca, published_at=published_at)
1626
+ _schedule_x_post_for_agent(payload, res)
1627
+ _tui_session_published += 1
1628
+ _tui_session_last_name = n or '—'
1629
+ run_delayed_claim_pass(AGENTS_REGISTRY_PATH, pkey or '', swarms_key, CLAIM_DELAY_HOURS)
1630
+ if PERSISTENT_LOOP_SLEEP_SECONDS > 0:
1631
+ time.sleep(PERSISTENT_LOOP_SLEEP_SECONDS)
1632
+
1633
+ def run_standalone_cli() -> None:
1634
+ run_profile_selection()
1635
+ _title = _style_heading('LUAF — brief → research → launch')
1636
+ _opts = _style_muted(' 1. Pipeline (design & launch autonomous unit)\n 2. Persistent (autonomous loop until target SOL)\n 0. Exit')
1637
+ _menu = '\n ╭─────────────────────────────────────╮\n │ ' + _title + ' │\n ╰─────────────────────────────────────╯\n' + _opts + '\n'
1638
+ _handlers: dict[str, Any] = {'1': main, '2': run_persistent, '0': None}
1639
+ while True:
1640
+ print(_menu)
1641
+ try:
1642
+ c = input(' Choice [1]: ').strip() or '1'
1643
+ except (EOFError, KeyboardInterrupt):
1644
+ print()
1645
+ return
1646
+ if c == '0':
1647
+ return
1648
+ fn = _handlers.get(c)
1649
+ if fn is not None:
1650
+ fn()
1651
+ else:
1652
+ print(_style_warn(' Unknown. Use 1 (Pipeline), 2 (Autonomous loop), or 0 (Exit).'))
1653
+
1654
+ def run_interactive_menu() -> None:
1655
+ if create_luaf_app:
1656
+ def _set_stop() -> None:
1657
+ global _tui_stop_requested
1658
+ _tui_stop_requested = True
1659
+ def _get_state() -> tuple[str, int, str, str]:
1660
+ return (_tui_current_topic, _tui_session_published, _tui_session_last_name, _tui_stopped_reason)
1661
+ default = _get_default_profile()
1662
+ profile_options_list: list[dict[str, Any]] = [default]
1663
+ if _list_profiles is not None and PROFILES_DIR.is_dir():
1664
+ profile_options_list = [default] + _list_profiles(PROFILES_DIR)
1665
+ def _on_profile_selected(idx: int) -> None:
1666
+ global _active_profile
1667
+ if 0 <= idx < len(profile_options_list):
1668
+ _active_profile = profile_options_list[idx]
1669
+ logger.info('Profile: {}', _active_profile.get('display_name', _active_profile.get('id', 'default')))
1670
+ config: dict[str, Any] = {'get_creator_pubkey': get_creator_pubkey, 'get_solana_balance': get_solana_balance, 'load_agents_registry': lambda: _load_agents_registry(AGENTS_REGISTRY_PATH), 'target_sol': PERSISTENT_TARGET_SOL, 'registry_path': AGENTS_REGISTRY_PATH, 'rpc_url': SOLANA_RPC_URL, 'set_stop_requested': _set_stop, 'get_tui_state': _get_state, 'log_queue': _log_queue, 'profile_options': profile_options_list, 'on_profile_selected': _on_profile_selected}
1671
+ LUAFApp = create_luaf_app(run_persistent, config)
1672
+ app = LUAFApp()
1673
+ app.run()
1674
+ else:
1675
+ run_standalone_cli()
1676
+
1677
+ def main() -> None:
1678
+ try:
1679
+ org = _luaf_get_current_organism()
1680
+ if org.get('planner_weights_path'):
1681
+ os.environ['LUAF_PLANNER_WEIGHTS'] = str(org['planner_weights_path'])
1682
+ if (org.get('config') or {}).get('template_id'):
1683
+ os.environ['LUAF_TEMPLATE'] = str(org['config']['template_id'])
1684
+ except Exception:
1685
+ pass
1686
+ api_key = os.environ.get('OPENAI_API_KEY', '').strip()
1687
+ base_url = (os.environ.get('OPENAI_BASE_URL') or 'https://api.openai.com/v1').strip()
1688
+ swarms_key = (os.environ.get('SWARMS_API_KEY') or SWARMS_API_KEY_FALLBACK or '').strip()
1689
+ if not api_key:
1690
+ logger.error('OPENAI_API_KEY not set')
1691
+ return
1692
+ if not swarms_key and (not DRY_RUN):
1693
+ logger.warning('SWARMS_API_KEY not set')
1694
+ pkey = get_private_key_from_env()
1695
+ cwallet = (os.environ.get('SOLANA_PUBKEY') or os.environ.get('CREATOR_WALLET') or '').strip()
1696
+ if not DRY_RUN and (not (pkey or '').strip()):
1697
+ logger.warning('No SOLANA_PRIVATE_KEY; skipping publish.')
1698
+ if not DRY_RUN and pkey and (not cwallet):
1699
+ logger.warning('No CREATOR_WALLET; tokenized publish may fail.')
1700
+ if DRY_RUN:
1701
+ logger.info('Dry run mode.')
1702
+ brief = read_design_brief_interactive()
1703
+ if not (brief or '').strip():
1704
+ brief = _generate_topic_via_llm(api_key, base_url) or _DEFAULT_TOPIC
1705
+ logger.info('Brief was blank; generated: {}', brief[:200] + '...' if len(brief) > 200 else brief)
1706
+ name_override, ticker_override = read_optional_name_and_ticker()
1707
+ if name_override:
1708
+ brief = f'{brief}\n(Use exactly this unit name: {name_override})'
1709
+ if ticker_override:
1710
+ brief = f'{brief}\n(Use exactly this ticker: {ticker_override})'
1711
+ logger.info('Brief: {}', brief[:200] + '...' if len(brief) > 200 else brief)
1712
+ used_n: set[str] = set()
1713
+ used_t: set[str] = set()
1714
+ vfb: Optional[str] = None
1715
+ tmpl = (os.environ.get('LUAF_TEMPLATE') or '').strip() or None
1716
+ for step in range(1, MAX_STEPS + 1):
1717
+ logger.info('Step {}/{}', step, MAX_STEPS)
1718
+ if USE_MULTIHOP_WEB_RAG:
1719
+ snip = _multihop_web_rag(brief, max_hops=RAG_MAX_HOPS, threshold=RAG_CONVERGE_THRESHOLD, total_k=RAG_TOTAL_K, ddg_per_hop=RAG_DDG_PER_HOP)
1720
+ if not snip:
1721
+ snip = search_duckduckgo(f'{brief} {random.choice(SEARCH_VARIANT_SUFFIXES)}', max_results=DUCKDUCKGO_MAX_RESULTS)
1722
+ else:
1723
+ snip = search_duckduckgo(f'{brief} {random.choice(SEARCH_VARIANT_SUFFIXES)}', max_results=DUCKDUCKGO_MAX_RESULTS)
1724
+ snip = _append_keyless_api_search(brief, snip)
1725
+ payload = None
1726
+ if USE_PLANNER and _plan_from_topic_and_search and _execute_plan and _get_template:
1727
+ try:
1728
+ plan = _plan_from_topic_and_search(brief, snip, use_model=True)
1729
+ payload = _execute_plan(plan, _get_template, required_payload_keys=REQUIRED_PAYLOAD_KEYS)
1730
+ logger.info('Planner OK (template={})', plan.get('template_id', ''))
1731
+ code = str(payload.get('agent') or '')
1732
+ if _is_skeleton_agent_code(code):
1733
+ if USE_DESIGNER:
1734
+ logger.info('Skeleton detected; expanding via designer LLM.')
1735
+ tmpl = (plan.get('template_id') or '').strip() or tmpl
1736
+ pname, pticker = (plan.get('name') or '', plan.get('ticker') or '')
1737
+ if pname or pticker:
1738
+ brief = f'{brief}\n(Preferred: name={pname}, ticker={pticker})'
1739
+ payload = None
1740
+ else:
1741
+ logger.error('Planner produced skeleton only. Set LUAF_USE_DESIGNER=1 to expand via LLM.')
1742
+ payload = None
1743
+ break
1744
+ except Exception as e:
1745
+ logger.warning('Planner failed: {}', e)
1746
+ payload = None
1747
+ if payload is None and USE_DESIGNER:
1748
+ try:
1749
+ retrieved = _retrieve_similar_exemplars(brief, snip, top_k=3)
1750
+ if (os.environ.get('LUAF_DESIGNER_SUBPROCESS', '1') or '').strip().lower() not in ('0', 'false', 'no'):
1751
+ raw = _run_designer_in_subprocess(topic=brief, search_snippets=snip, model=LLM_MODEL, api_key=api_key, base_url=base_url, existing_names=used_n, existing_tickers=used_t, temperature=LLM_TEMPERATURE, validation_feedback=vfb, template_id=tmpl, retrieved_exemplars=retrieved)
1752
+ else:
1753
+ raw = get_agent_payload_from_llm(topic=brief, search_snippets=snip, model=LLM_MODEL, api_key=api_key, base_url=base_url, existing_names=used_n, existing_tickers=used_t, temperature=LLM_TEMPERATURE, validation_feedback=vfb, template_id=tmpl, retrieved_exemplars=retrieved)
1754
+ except Exception as e:
1755
+ logger.error('LLM failed: {}', e)
1756
+ vfb = f'LLM: {e!s}'
1757
+ continue
1758
+ try:
1759
+ payload = parse_agent_payload(raw)
1760
+ except ValueError as e:
1761
+ logger.error('Parse: {}', e)
1762
+ vfb = f'Parse: {e!s}'
1763
+ continue
1764
+ if payload is None and (not USE_DESIGNER) and (not USE_PLANNER or not _plan_from_topic_and_search):
1765
+ logger.error('No planner or designer. Set LUAF_USE_DESIGNER=1.')
1766
+ break
1767
+ if payload is None:
1768
+ continue
1769
+ if name_override:
1770
+ payload['name'] = name_override
1771
+ if ticker_override:
1772
+ payload['ticker'] = ticker_override
1773
+ code = str(payload.get('agent') or '')
1774
+ sk_fb = _skeleton_validation_feedback(code)
1775
+ if sk_fb is not None:
1776
+ vfb = sk_fb
1777
+ logger.warning('Unit code too short or skeleton (step {}): {}', step, sk_fb[:200])
1778
+ continue
1779
+ saved_path = _save_generated_agent(code, payload.get('name'), payload.get('ticker'), step)
1780
+ ok, fb = run_agent_code_validation(code, VALIDATION_TIMEOUT)
1781
+ if ok:
1782
+ logger.info('Validation OK (step {}). Publishing to marketplace (dry_run={}).', step, DRY_RUN)
1783
+ n, t = ((payload.get('name') or '').strip(), (payload.get('ticker') or '').strip())
1784
+ used_n.add(n.lower())
1785
+ used_t.add(t.upper())
1786
+ res = publish_agent(payload, swarms_key, pkey or '', DRY_RUN, creator_wallet=cwallet)
1787
+ logger.info('Publish returned; updating registry and X only if not dry run.')
1788
+ if res and (not DRY_RUN):
1789
+ lu, rid, ca = (res.get('listing_url'), res.get('id'), res.get('token_address'))
1790
+ if lu or rid or ca:
1791
+ append_agent_to_registry(AGENTS_REGISTRY_PATH, name=n, ticker=t, listing_url=lu, id_=rid, token_address=ca)
1792
+ _schedule_x_post_for_agent(payload, res)
1793
+ if USE_RUN_IN_NEW_TERMINAL and saved_path:
1794
+ run_agent_in_new_terminal(saved_path, (brief or '').strip() or 'Run a quick check.')
1795
+ break
1796
+ vfb = fb
1797
+ logger.warning('Unit validation failed (step {}): {}', step, fb[:1500])
1798
+ logger.info('Validation full feedback (step {}):\n{}', step, fb[:3000] + ('...' if len(fb) > 3000 else ''))
1799
+ if _ask_publish_without_validation():
1800
+ logger.info('Publishing without validation (user confirmed). dry_run={}', DRY_RUN)
1801
+ n, t = ((payload.get('name') or '').strip(), (payload.get('ticker') or '').strip())
1802
+ used_n.add(n.lower())
1803
+ used_t.add(t.upper())
1804
+ res = publish_agent(payload, swarms_key, pkey or '', DRY_RUN, creator_wallet=cwallet)
1805
+ logger.info('Publish returned.')
1806
+ if res and (not DRY_RUN):
1807
+ lu, rid, ca = (res.get('listing_url'), res.get('id'), res.get('token_address'))
1808
+ if lu or rid or ca:
1809
+ append_agent_to_registry(AGENTS_REGISTRY_PATH, name=n, ticker=t, listing_url=lu, id_=rid, token_address=ca)
1810
+ _schedule_x_post_for_agent(payload, res)
1811
+ if USE_RUN_IN_NEW_TERMINAL and saved_path:
1812
+ run_agent_in_new_terminal(saved_path, (brief or '').strip() or 'Run a quick check.')
1813
+ break
1814
+ else:
1815
+ logger.warning('Max steps ({}) reached.', MAX_STEPS)
1816
+ if CLAIM_FEES_AFTER_RUN and (pkey or '').strip() and not DRY_RUN:
1817
+ reg = _load_agents_registry(AGENTS_REGISTRY_PATH)
1818
+ claimable = [e for e in reg if (e.get('token_address') or e.get('ca')) and len((e.get('token_address') or e.get('ca') or '')) >= 32]
1819
+ if claimable:
1820
+ logger.info('Claiming fees for {} agent(s)...', len(claimable))
1821
+ for e in claimable:
1822
+ ca = e.get('token_address') or e.get('ca')
1823
+ if not ca or len(ca) < 32:
1824
+ continue
1825
+ logger.info('Claiming fees for {}', ca[:16])
1826
+ cr = claim_fees(ca, pkey.strip(), api_key=swarms_key)
1827
+ if cr and cr.get('success'):
1828
+ logger.info('Claimed: sig={} sol={}', cr.get('signature'), cr.get('amountClaimedSol'))
1829
+ elif cr:
1830
+ logger.warning('Claim: {}', cr)
1831
+ else:
1832
+ logger.debug('No claimable agents in registry.')
1833
+ else:
1834
+ if DRY_RUN and CLAIM_FEES_AFTER_RUN:
1835
+ logger.debug('Skipping claim fees (dry run).')
1836
+ logger.info('Draining X queue (if enabled)...')
1837
+ _drain_x_queue_if_enabled()
1838
+ logger.info('Done.')
1839
+ if _luaf_should_evolve():
1840
+ try:
1841
+ _, upd = _luaf_run_evolution()
1842
+ if upd:
1843
+ logger.info('Evolution: organism updated.')
1844
+ except Exception as e:
1845
+ logger.warning('Evolution: {}', e)
1846
+ if os.environ.get('LUAF_BACKGROUND_TRAIN', '').strip() != '0':
1847
+ try:
1848
+ ta = (brief or '').strip()[:500]
1849
+ if ta:
1850
+ subprocess.Popen([sys.executable, str(_luaf_agent_dir() / 'LUAF.py'), '--self-train', ta], cwd=str(_luaf_agent_dir()), env=os.environ.copy(), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1851
+ except Exception:
1852
+ pass
1853
+ if os.environ.get('LUAF_MOLTBOOK_SOCIAL', '').strip() == '1' and _run_social_autonomy:
1854
+ try:
1855
+ if _run_social_autonomy(brief):
1856
+ logger.info('Social: sent.')
1857
+ except Exception:
1858
+ pass
1859
+
1860
+ # Init wizard hints (where to get keys). Design spec: tui.css / plan.
1861
+ _INIT_REQUIRED_KEYS = ('OPENAI_API_KEY', 'OPENAI_BASE_URL', 'SWARMS_API_KEY', 'SOLANA_PUBKEY')
1862
+ _INIT_OPTIONAL_KEYS = ('SOLANA_PRIVATE_KEY', 'X_API_KEY', 'X_API_SECRET', 'X_ACCESS_TOKEN', 'X_ACCESS_SECRET')
1863
+ _INIT_HINTS: dict[str, str] = {
1864
+ 'OPENAI_API_KEY': 'Create at https://platform.openai.com/api-keys (API keys → Create new secret key)',
1865
+ 'OPENAI_BASE_URL': 'Default: https://api.openai.com/v1 — change only if using a proxy or compatible endpoint',
1866
+ 'SWARMS_API_KEY': 'Get from https://swarms.world (Swarms dashboard / sign-up)',
1867
+ 'SOLANA_PUBKEY': 'Your Solana wallet public key (base58). From Phantom/Solflare or: solana address',
1868
+ 'SOLANA_PRIVATE_KEY': 'Base58 secret key or path in SOLANA_PRIVATE_KEY_FILE; needed for publishing. Export from wallet or generate with Solana CLI',
1869
+ 'X_API_KEY': 'X (Twitter) app API key: https://developer.x.com — Project → App → Keys and tokens',
1870
+ 'X_API_SECRET': 'X app API secret (same app)',
1871
+ 'X_ACCESS_TOKEN': 'X OAuth 1.0a access token (user context)',
1872
+ 'X_ACCESS_SECRET': 'X OAuth 1.0a access token secret',
1873
+ }
1874
+
1875
+ def _parse_env_file(path: Path) -> dict[str, str]:
1876
+ """Read .env-style file into key -> value. Comments and empty lines omitted."""
1877
+ out: dict[str, str] = {}
1878
+ if not path.exists():
1879
+ return out
1880
+ for line in path.read_text(encoding='utf-8', errors='replace').splitlines():
1881
+ line = line.strip()
1882
+ if not line or line.startswith('#'):
1883
+ continue
1884
+ if '=' in line:
1885
+ k, _, v = line.partition('=')
1886
+ k = k.strip()
1887
+ if k:
1888
+ v = v.strip().strip('"').strip("'")
1889
+ out[k] = v
1890
+ return out
1891
+
1892
+ def _write_env_updates(path: Path, updates: dict[str, str], template_lines: list[str]) -> None:
1893
+ """Write .env file: apply updates to keys that appear in template_lines, else append."""
1894
+ existing = _parse_env_file(path)
1895
+ existing.update(updates)
1896
+ seen: set[str] = set()
1897
+ result: list[str] = []
1898
+ for line in template_lines:
1899
+ stripped = line.strip()
1900
+ if stripped and '=' in stripped and not stripped.startswith('#'):
1901
+ k = stripped.partition('=')[0].strip()
1902
+ if k in existing:
1903
+ seen.add(k)
1904
+ result.append(f'{k}={existing[k]}')
1905
+ continue
1906
+ result.append(line.rstrip())
1907
+ for k, v in existing.items():
1908
+ if k not in seen:
1909
+ result.append(f'{k}={v}')
1910
+ path.write_text('\n'.join(result) + '\n', encoding='utf-8')
1911
+
1912
+ def run_init(init_args: Any) -> int:
1913
+ """Create or update .env; interactive prompts with hints. Returns exit code (0 = success)."""
1914
+ from_example = getattr(init_args, 'from_example', False)
1915
+ force = getattr(init_args, 'force', False)
1916
+ check = getattr(init_args, 'check', False)
1917
+ env_path = _REPO_ROOT / '.env'
1918
+ example_path = _REPO_ROOT / '.env.example'
1919
+ if not example_path.exists():
1920
+ example_path = _LUAF_DIR / '.env.example'
1921
+ if check:
1922
+ load_dotenv(env_path)
1923
+ load_dotenv(Path.cwd() / '.env')
1924
+ missing = [k for k in _INIT_REQUIRED_KEYS if not (os.environ.get(k) or '').strip()]
1925
+ if not missing:
1926
+ print(_style_success('All required env vars are set.'))
1927
+ return 0
1928
+ print(_style_error('Missing required env vars: ') + ', '.join(missing))
1929
+ return 1
1930
+ template_lines: list[str] = []
1931
+ if example_path.exists():
1932
+ template_lines = example_path.read_text(encoding='utf-8', errors='replace').splitlines()
1933
+ else:
1934
+ template_lines = [
1935
+ '# LUAF — copy to .env and fill in. Do not commit .env (secrets).',
1936
+ '',
1937
+ '# Required for trend → spec → agent pipeline',
1938
+ 'OPENAI_API_KEY=sk-proj-your-openai-key-here',
1939
+ 'OPENAI_BASE_URL=https://api.openai.com/v1',
1940
+ 'SWARMS_API_KEY=sk-your-swarms-api-key-here',
1941
+ 'SOLANA_PUBKEY=YourSolanaWalletPublicKeyBase58',
1942
+ '',
1943
+ ]
1944
+ if not env_path.exists():
1945
+ env_path.write_text('\n'.join(template_lines) + '\n', encoding='utf-8')
1946
+ print(_style_success(f'Created .env at {env_path}'))
1947
+ else:
1948
+ if from_example:
1949
+ return 0
1950
+ print(_style_muted(f'.env exists at {env_path}'))
1951
+ current = _parse_env_file(env_path)
1952
+ is_tty = getattr(sys.stdin, 'isatty', lambda: False)()
1953
+ if not is_tty or from_example:
1954
+ return 0
1955
+ getpass = __import__('getpass', fromlist=['getpass']).getpass
1956
+ updates: dict[str, str] = {}
1957
+ for key in _INIT_REQUIRED_KEYS:
1958
+ existing = (current.get(key) or '').strip()
1959
+ if existing and not force:
1960
+ continue
1961
+ hint = _INIT_HINTS.get(key, '')
1962
+ print(_style_heading(f' {key}'))
1963
+ if hint:
1964
+ print(_style_muted(f' {hint}'))
1965
+ if key in ('OPENAI_API_KEY', 'SWARMS_API_KEY'):
1966
+ val = getpass(f' Value (or Enter to skip): ').strip()
1967
+ else:
1968
+ try:
1969
+ val = input(f' Value (or Enter to skip): ').strip()
1970
+ except (EOFError, KeyboardInterrupt):
1971
+ val = ''
1972
+ if val:
1973
+ updates[key] = val
1974
+ if updates:
1975
+ _write_env_updates(env_path, updates, template_lines)
1976
+ print(_style_success('Updated .env with provided values.'))
1977
+ if is_tty and not from_example:
1978
+ try:
1979
+ opt = input(_style_heading('\n Set up optional keys (Solana private key, X posting)? [y/N]: ')).strip().lower()
1980
+ if opt in ('y', 'yes'):
1981
+ print(_style_muted(' Optional keys — press Enter to skip any.\n'))
1982
+ for key in _INIT_OPTIONAL_KEYS:
1983
+ existing = (current.get(key) or updates.get(key) or '').strip()
1984
+ if existing and not force:
1985
+ continue
1986
+ hint = _INIT_HINTS.get(key, '')
1987
+ print(_style_heading(f' {key}'))
1988
+ if hint:
1989
+ print(_style_muted(f' {hint}'))
1990
+ if key in ('SOLANA_PRIVATE_KEY', 'X_API_SECRET', 'X_ACCESS_SECRET'):
1991
+ val = getpass(f' Value (or Enter to skip): ').strip()
1992
+ else:
1993
+ try:
1994
+ val = input(f' Value (or Enter to skip): ').strip()
1995
+ except (EOFError, KeyboardInterrupt):
1996
+ val = ''
1997
+ if val:
1998
+ updates[key] = val
1999
+ if updates:
2000
+ current = _parse_env_file(env_path)
2001
+ current.update(updates)
2002
+ _write_env_updates(env_path, current, template_lines)
2003
+ print(_style_success(' Optional keys saved.'))
2004
+ except (EOFError, KeyboardInterrupt):
2005
+ pass
2006
+ print(_style_heading('\n Next steps:'))
2007
+ print(_style_muted(' luaf doctor — check config and connectivity'))
2008
+ print(_style_muted(' luaf run — run a single pipeline'))
2009
+ print(_style_muted(' luaf persistent — autonomous loop until target SOL'))
2010
+ return 0
2011
+
2012
+ def run_doctor(doctor_args: Any) -> int:
2013
+ """Check .env, required vars, optional vars, and basic connectivity. Returns 0 if required OK, 1 otherwise."""
2014
+ load_dotenv(_REPO_ROOT / '.env')
2015
+ load_dotenv(Path.cwd() / '.env')
2016
+ env_path = _REPO_ROOT / '.env'
2017
+ issues: list[str] = []
2018
+ ok_items: list[str] = []
2019
+ if not env_path.exists():
2020
+ issues.append('.env not found (run: luaf init)')
2021
+ else:
2022
+ ok_items.append('.env exists')
2023
+ for k in _INIT_REQUIRED_KEYS:
2024
+ v = (os.environ.get(k) or '').strip()
2025
+ if not v:
2026
+ issues.append(f'Missing required: {k}')
2027
+ else:
2028
+ ok_items.append(f'{k} set')
2029
+ x_keys = ('X_API_KEY', 'X_API_SECRET', 'X_ACCESS_TOKEN', 'X_ACCESS_SECRET')
2030
+ x_set = sum(1 for k in x_keys if (os.environ.get(k) or '').strip())
2031
+ if x_set > 0 and x_set < 4:
2032
+ issues.append('X posting: set all four X_* vars or leave all unset')
2033
+ elif x_set == 4:
2034
+ ok_items.append('X posting credentials set')
2035
+ if (os.environ.get('SOLANA_PRIVATE_KEY') or '').strip() or (os.environ.get('SOLANA_PRIVATE_KEY_FILE') or '').strip():
2036
+ ok_items.append('Solana private key configured (publish enabled)')
2037
+ else:
2038
+ ok_items.append('Solana private key not set (publish will be dry-run only)')
2039
+ for s in ok_items:
2040
+ print(_style_success(' ✓ ') + s)
2041
+ for s in issues:
2042
+ print(_style_error(' ✗ ') + s)
2043
+ if issues:
2044
+ bal = None
2045
+ try:
2046
+ pubkey = get_creator_pubkey()
2047
+ if pubkey:
2048
+ bal = get_solana_balance(pubkey, SOLANA_RPC_URL)
2049
+ print(_style_info(f' Solana balance: {bal:.4f} SOL') + ' (at ' + (pubkey[:8] + '...') + ')')
2050
+ except Exception:
2051
+ pass
2052
+ print(_style_warn('\n Fix missing vars with: luaf init'))
2053
+ return 1
2054
+ try:
2055
+ pubkey = get_creator_pubkey()
2056
+ if pubkey:
2057
+ bal = get_solana_balance(pubkey, SOLANA_RPC_URL)
2058
+ print(_style_success(f' Solana balance: {bal:.4f} SOL'))
2059
+ except Exception as e:
2060
+ print(_style_warn(f' Solana check: {e}'))
2061
+ print(_style_success('\n Doctor: required config OK.'))
2062
+ return 0
2063
+
2064
+ def _build_parser() -> Any:
2065
+ import argparse
2066
+ p = argparse.ArgumentParser(description='LUAF: brief -> research -> build -> validate -> launch autonomous business units.', formatter_class=argparse.RawDescriptionHelpFormatter, epilog='\nExamples:\n luaf CLI menu (default; use --tui for experimental TUI)\n luaf init Setup wizard: .env and API keys\n luaf init --check Verify required env vars\n luaf doctor Check config and connectivity\n luaf run Single pipeline\n luaf persistent Autonomous loop until target SOL\n luaf help Show this help\n')
2067
+ p.add_argument('--no-tui', '-n', action='store_true', help='Use CLI menu only (default)')
2068
+ p.add_argument('--tui', action='store_true', help='Use TUI (experimental); default is CLI')
2069
+ p.add_argument('--no-color', action='store_true', help='Disable ANSI colors (also NO_COLOR=1)')
2070
+ p.add_argument('--once', '-o', action='store_true', help='Run single pipeline (same as run)')
2071
+ p.add_argument('--persistent', '-p', action='store_true', help='Run autonomous loop until target SOL')
2072
+ p.add_argument('--self-train', metavar='TOPIC', nargs='?', const='', default=None, help='Run self-train pipeline; TOPIC optional')
2073
+ sub = p.add_subparsers(dest='command', help='Command')
2074
+ init_p = sub.add_parser('init', help='Setup wizard: create .env and prompt for API keys')
2075
+ init_p.add_argument('--from-example', action='store_true', help='Non-interactive: only ensure .env exists from template')
2076
+ init_p.add_argument('--force', action='store_true', help='Allow overwriting existing keys when re-prompting')
2077
+ init_p.add_argument('--check', action='store_true', help='Validate required env vars are set; exit 0/1')
2078
+ sub.add_parser('run', help='Run single pipeline and exit')
2079
+ sub.add_parser('persistent', help='Run autonomous loop until target SOL')
2080
+ st = sub.add_parser('self-train', help='Run self-train pipeline')
2081
+ st.add_argument('topic', nargs='?', default='', help='Topic (default from env/TOPIC)')
2082
+ sub.add_parser('doctor', help='Check config, env vars, and connectivity')
2083
+ sub.add_parser('help', help='Show help and exit')
2084
+ return p
2085
+
2086
+ def _parse_cli() -> Any:
2087
+ import argparse
2088
+ set_cli_no_color((os.environ.get('NO_COLOR') or '').strip() != '')
2089
+ p = _build_parser()
2090
+ args = p.parse_args()
2091
+ if getattr(args, 'no_color', False):
2092
+ set_cli_no_color(True)
2093
+ if getattr(args, 'once', False):
2094
+ args.command = 'run'
2095
+ if getattr(args, 'persistent', False):
2096
+ args.command = 'persistent'
2097
+ if args.self_train is not None:
2098
+ args.command = 'self-train'
2099
+ if not hasattr(args, 'topic'):
2100
+ args.topic = args.self_train if args.self_train else ''
2101
+ for a in sys.argv[1:]:
2102
+ a = a.strip().lower()
2103
+ if a == 'run':
2104
+ args.command = 'run'
2105
+ break
2106
+ if a in ('persistent', '--persistent'):
2107
+ args.command = 'persistent'
2108
+ break
2109
+ if os.environ.get('LUAF_MODE', '').strip().lower() == 'persistent' and (not args.command or args.command not in ('run', 'self-train')):
2110
+ args.command = 'persistent'
2111
+ if not getattr(args, 'command', None) and (getattr(args, 'once', False) or getattr(args, 'persistent', False)):
2112
+ args.command = 'persistent' if getattr(args, 'persistent', False) else 'run'
2113
+ return args
2114
+
2115
+ def run_cli() -> None:
2116
+ """Entry point for the `luaf` console script. Parses CLI and dispatches to init, run, persistent, or interactive menu."""
2117
+ args = _parse_cli()
2118
+ cmd = getattr(args, 'command', None)
2119
+ if cmd == 'init':
2120
+ sys.exit(run_init(args))
2121
+ if cmd == 'self-train':
2122
+ topic = (getattr(args, 'topic', '') or TOPIC).strip()[:500] or TOPIC
2123
+ sys.exit(0 if _luaf_run_self_train(topic) else 1)
2124
+ if cmd == 'persistent':
2125
+ run_profile_selection()
2126
+ run_persistent()
2127
+ elif cmd == 'run':
2128
+ run_profile_selection()
2129
+ main()
2130
+ elif cmd == 'doctor':
2131
+ sys.exit(run_doctor(args))
2132
+ elif cmd == 'help':
2133
+ _build_parser().print_help()
2134
+ sys.exit(0)
2135
+ elif cmd is not None:
2136
+ print(_style_error(f'Unknown command: {cmd}'))
2137
+ sys.exit(1)
2138
+ elif getattr(args, 'tui', False):
2139
+ run_interactive_menu()
2140
+ else:
2141
+ run_standalone_cli()
2142
+
2143
+
2144
+ if __name__ == '__main__':
2145
+ run_cli()