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 +2145 -0
- luaf-1.2.0.dist-info/METADATA +297 -0
- luaf-1.2.0.dist-info/RECORD +12 -0
- luaf-1.2.0.dist-info/WHEEL +5 -0
- luaf-1.2.0.dist-info/entry_points.txt +2 -0
- luaf-1.2.0.dist-info/licenses/LICENSE +21 -0
- luaf-1.2.0.dist-info/top_level.txt +6 -0
- luaf_designer.py +209 -0
- luaf_profiles.py +87 -0
- luaf_publish.py +190 -0
- luaf_tui.py +262 -0
- luaf_x_post.py +276 -0
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()
|