devrel-origin 0.2.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. devrel_origin/__init__.py +15 -0
  2. devrel_origin/cli/__init__.py +92 -0
  3. devrel_origin/cli/_common.py +243 -0
  4. devrel_origin/cli/analytics.py +28 -0
  5. devrel_origin/cli/argus.py +497 -0
  6. devrel_origin/cli/auth.py +227 -0
  7. devrel_origin/cli/config.py +108 -0
  8. devrel_origin/cli/content.py +259 -0
  9. devrel_origin/cli/cost.py +108 -0
  10. devrel_origin/cli/cro.py +298 -0
  11. devrel_origin/cli/deliverables.py +65 -0
  12. devrel_origin/cli/docs.py +91 -0
  13. devrel_origin/cli/doctor.py +178 -0
  14. devrel_origin/cli/experiment.py +29 -0
  15. devrel_origin/cli/growth.py +97 -0
  16. devrel_origin/cli/init.py +472 -0
  17. devrel_origin/cli/intel.py +27 -0
  18. devrel_origin/cli/kb.py +96 -0
  19. devrel_origin/cli/listen.py +31 -0
  20. devrel_origin/cli/marketing.py +66 -0
  21. devrel_origin/cli/migrate.py +45 -0
  22. devrel_origin/cli/run.py +46 -0
  23. devrel_origin/cli/sales.py +57 -0
  24. devrel_origin/cli/schedule.py +62 -0
  25. devrel_origin/cli/synthesize.py +28 -0
  26. devrel_origin/cli/triage.py +29 -0
  27. devrel_origin/cli/video.py +35 -0
  28. devrel_origin/core/__init__.py +58 -0
  29. devrel_origin/core/agent_config.py +75 -0
  30. devrel_origin/core/argus.py +964 -0
  31. devrel_origin/core/atlas.py +1450 -0
  32. devrel_origin/core/base.py +372 -0
  33. devrel_origin/core/cyra.py +563 -0
  34. devrel_origin/core/dex.py +708 -0
  35. devrel_origin/core/echo.py +614 -0
  36. devrel_origin/core/growth/__init__.py +27 -0
  37. devrel_origin/core/growth/recommendations.py +219 -0
  38. devrel_origin/core/growth/target_kinds.py +51 -0
  39. devrel_origin/core/iris.py +513 -0
  40. devrel_origin/core/kai.py +1367 -0
  41. devrel_origin/core/llm.py +542 -0
  42. devrel_origin/core/llm_backends.py +274 -0
  43. devrel_origin/core/mox.py +514 -0
  44. devrel_origin/core/nova.py +349 -0
  45. devrel_origin/core/pax.py +1205 -0
  46. devrel_origin/core/rex.py +532 -0
  47. devrel_origin/core/sage.py +486 -0
  48. devrel_origin/core/sentinel.py +385 -0
  49. devrel_origin/core/types.py +98 -0
  50. devrel_origin/core/video/__init__.py +22 -0
  51. devrel_origin/core/video/assembler.py +131 -0
  52. devrel_origin/core/video/browser_recorder.py +118 -0
  53. devrel_origin/core/video/desktop_recorder.py +254 -0
  54. devrel_origin/core/video/overlay_renderer.py +143 -0
  55. devrel_origin/core/video/script_parser.py +147 -0
  56. devrel_origin/core/video/tts_engine.py +82 -0
  57. devrel_origin/core/vox.py +268 -0
  58. devrel_origin/core/watchdog.py +321 -0
  59. devrel_origin/project/__init__.py +1 -0
  60. devrel_origin/project/config.py +75 -0
  61. devrel_origin/project/cost_sink.py +61 -0
  62. devrel_origin/project/init.py +104 -0
  63. devrel_origin/project/paths.py +75 -0
  64. devrel_origin/project/state.py +241 -0
  65. devrel_origin/project/templates/__init__.py +4 -0
  66. devrel_origin/project/templates/config.toml +24 -0
  67. devrel_origin/project/templates/devrel.gitignore +10 -0
  68. devrel_origin/project/templates/slop-blocklist.md +45 -0
  69. devrel_origin/project/templates/style.md +24 -0
  70. devrel_origin/project/templates/voice.md +29 -0
  71. devrel_origin/quality/__init__.py +66 -0
  72. devrel_origin/quality/editorial.py +357 -0
  73. devrel_origin/quality/persona.py +84 -0
  74. devrel_origin/quality/readability.py +148 -0
  75. devrel_origin/quality/slop.py +167 -0
  76. devrel_origin/quality/style.py +110 -0
  77. devrel_origin/quality/voice.py +15 -0
  78. devrel_origin/tools/__init__.py +9 -0
  79. devrel_origin/tools/analytics.py +304 -0
  80. devrel_origin/tools/api_client.py +393 -0
  81. devrel_origin/tools/apollo_client.py +305 -0
  82. devrel_origin/tools/code_validator.py +428 -0
  83. devrel_origin/tools/github_tools.py +297 -0
  84. devrel_origin/tools/instantly_client.py +412 -0
  85. devrel_origin/tools/kb_harvester.py +340 -0
  86. devrel_origin/tools/mcp_server.py +578 -0
  87. devrel_origin/tools/notifications.py +245 -0
  88. devrel_origin/tools/run_report.py +193 -0
  89. devrel_origin/tools/scheduler.py +231 -0
  90. devrel_origin/tools/search_tools.py +321 -0
  91. devrel_origin/tools/self_improve.py +168 -0
  92. devrel_origin/tools/sheets.py +236 -0
  93. devrel_origin-0.2.14.dist-info/METADATA +354 -0
  94. devrel_origin-0.2.14.dist-info/RECORD +98 -0
  95. devrel_origin-0.2.14.dist-info/WHEEL +5 -0
  96. devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
  97. devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
  98. devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,532 @@
1
+ """
2
+ Rex — Competitive Intelligence Agent
3
+
4
+ Monitors the competitive landscape and produces actionable intelligence
5
+ that informs sales positioning, product strategy, and marketing messaging.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import re
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Any, Optional
15
+
16
+ if TYPE_CHECKING:
17
+ from devrel_origin.tools.apollo_client import ApolloClient
18
+
19
+ from devrel_origin.core.base import (
20
+ STOP_WORDS,
21
+ get_kb_search,
22
+ load_agent_prompt,
23
+ strip_markdown_fences,
24
+ )
25
+ from devrel_origin.core.llm import LLMClient
26
+ from devrel_origin.tools.api_client import PostHogClient
27
+ from devrel_origin.tools.search_tools import SearchTools
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ # Keywords near which capitalised words are treated as competitor names
33
+ COMPETITOR_KEYWORDS = {"vs", "alternative", "compared to", "competitor", "versus"}
34
+
35
+ # Extra stop words specific to Rex (competitive analysis keywords)
36
+ REX_STOP_WORDS = frozenset(
37
+ {
38
+ "write",
39
+ "technical",
40
+ "tutorial",
41
+ "addressing",
42
+ "developer",
43
+ "pain",
44
+ "point",
45
+ "analyze",
46
+ "analyse",
47
+ "competitive",
48
+ "landscape",
49
+ "report",
50
+ }
51
+ )
52
+
53
+ # Cap parallel web-search and Apollo enrichment fan-out so a 10-competitor
54
+ # task doesn't open 10 simultaneous Firecrawl/Brave/Apollo connections.
55
+ # 3 is conservative enough for free-tier API limits while still cutting
56
+ # wall-clock time roughly to ceil(N/3).
57
+ SEARCH_CONCURRENCY = 3
58
+
59
+
60
+ def _guess_domain(comp: str) -> str:
61
+ """Best-effort domain guess for a competitor name.
62
+
63
+ Preserves an existing TLD if the input already looks like a domain
64
+ (e.g. ``"Pendo.io"`` → ``"pendo.io"``, ``"FullStory"`` → ``"fullstory.com"``).
65
+ Strips spaces; the result is lowercased so callers can pass it
66
+ straight to Apollo enrichment.
67
+ """
68
+ cleaned = comp.strip().lower().replace(" ", "")
69
+ # If the name already contains a dot followed by 2+ alpha chars, treat
70
+ # it as an existing domain and don't append `.com` on top.
71
+ if re.search(r"\.[a-z]{2,}$", cleaned):
72
+ return cleaned
73
+ return f"{cleaned}.com"
74
+
75
+
76
+ @dataclass
77
+ class CompetitorProfile:
78
+ """A tracked competitor and their current market position."""
79
+
80
+ name: str
81
+ domain: str
82
+ category: str # e.g., "ai-assistant", "chatbot-platform"
83
+ strengths: list[str]
84
+ weaknesses: list[str]
85
+ recent_moves: list[str]
86
+
87
+
88
+ @dataclass
89
+ class MarketPosition:
90
+ """How a competitor positions themselves."""
91
+
92
+ competitor: str
93
+ positioning_statement: str
94
+ differentiators: list[str]
95
+ pricing_tier: str # "free", "freemium", "paid", "enterprise"
96
+ target_audience: str
97
+
98
+
99
+ @dataclass
100
+ class Threat:
101
+ """A competitive threat."""
102
+
103
+ competitor: str
104
+ threat: str
105
+ severity: str # "high", "medium", "low"
106
+
107
+
108
+ @dataclass
109
+ class Opportunity:
110
+ """A competitive gap/opportunity."""
111
+
112
+ gap: str
113
+ recommendation: str
114
+
115
+
116
+ @dataclass
117
+ class CompetitiveReport:
118
+ """Weekly competitive intelligence output."""
119
+
120
+ profiles: list[CompetitorProfile]
121
+ market_positions: list[MarketPosition]
122
+ threats: list[Threat]
123
+ opportunities: list[Opportunity]
124
+ recommended_responses: list[str]
125
+
126
+
127
+ class Rex:
128
+ """
129
+ Competitive Intelligence agent for monitoring the competitive landscape.
130
+
131
+ Capabilities:
132
+ - Discover competitors from task descriptions and the knowledge base
133
+ - Web-search each competitor for recent activity and positioning
134
+ - Produce competitor profiles with strengths/weaknesses
135
+ - Assess threats and opportunities with severity/impact ratings
136
+ - Generate actionable competitive intelligence reports
137
+
138
+ Tools:
139
+ 1. knowledge_base_search — Retrieve competitive mentions from docs
140
+ 2. web_search — Search the web for competitor activity
141
+ 3. llm_generate — Generate structured competitive analysis
142
+ """
143
+
144
+ _DEFAULT_SYSTEM_PROMPT = (
145
+ "You are Rex, a competitive intelligence analyst for {product_name}. "
146
+ "Your role is to monitor the competitive landscape and produce actionable "
147
+ "intelligence that informs sales positioning, product strategy, and "
148
+ "marketing messaging.\n\n"
149
+ "You produce:\n"
150
+ "- Weekly competitive landscape reports\n"
151
+ "- Competitor profiles with strengths/weaknesses\n"
152
+ "- Threat assessments with severity ratings\n"
153
+ "- Opportunity identification with recommended responses\n\n"
154
+ "Ground all analysis in evidence: social mentions, GitHub activity, "
155
+ "web search results, and knowledge base comparisons. "
156
+ "Never speculate without data."
157
+ )
158
+
159
+ @property
160
+ def SYSTEM_PROMPT_TEMPLATE(self) -> str:
161
+ return self._system_prompt_template
162
+
163
+ @property
164
+ def SYSTEM_PROMPT(self) -> str:
165
+ return self._system_prompt
166
+
167
+ def __init__(
168
+ self,
169
+ api_client: PostHogClient,
170
+ knowledge_base_path: Path,
171
+ llm_client: Optional[LLMClient] = None,
172
+ search_tools: Optional[SearchTools] = None,
173
+ apollo_client: Optional["ApolloClient"] = None, # NEW
174
+ product_name: str = "the target product",
175
+ ):
176
+ self.api_client = api_client
177
+ self.knowledge_base_path = knowledge_base_path
178
+ self.llm_client = llm_client
179
+ self.search_tools = search_tools
180
+ self.apollo_client = apollo_client # NEW
181
+ self.product_name = product_name
182
+ self._kb = get_kb_search(
183
+ knowledge_base_path,
184
+ extra_stop_words=REX_STOP_WORDS,
185
+ )
186
+ self._system_prompt_template = load_agent_prompt(
187
+ "rex", "system_prompt.txt", self._DEFAULT_SYSTEM_PROMPT
188
+ )
189
+ self._system_prompt = self._system_prompt_template.format(product_name=self.product_name)
190
+
191
+ # ------------------------------------------------------------------
192
+ # Competitor discovery
193
+ # ------------------------------------------------------------------
194
+
195
+ def _discover_competitors(self, task: str) -> list[str]:
196
+ """
197
+ Discover competitor names from the task string and the knowledge base.
198
+
199
+ Extraction sources:
200
+ 1. Explicit "for: X, Y, Z" pattern in the task string
201
+ 2. Capitalised words near competitor keywords in KB files
202
+ """
203
+ competitors: set[str] = set()
204
+
205
+ # 1. Parse from task string: "for: X, Y, Z"
206
+ match = re.search(r"for:\s*(.+)", task, re.IGNORECASE)
207
+ if match:
208
+ names = [n.strip() for n in match.group(1).split(",") if n.strip()]
209
+ competitors.update(names)
210
+
211
+ # 2. Scan knowledge base for capitalised words near competitor keywords
212
+ for _key, path in self._kb.index.items():
213
+ try:
214
+ content = path.read_text()
215
+ except Exception:
216
+ continue
217
+ content_lower = content.lower()
218
+ for keyword in COMPETITOR_KEYWORDS:
219
+ if keyword in content_lower:
220
+ # Capitalised word AFTER keyword: "vs Mixpanel"
221
+ after_pattern = r"(?i:" + re.escape(keyword) + r")\s+([A-Z][a-zA-Z]+)"
222
+ for m in re.finditer(after_pattern, content):
223
+ candidate = m.group(1).strip()
224
+ if (
225
+ len(candidate) > 1
226
+ and candidate.lower() not in STOP_WORDS
227
+ and candidate[0].isupper()
228
+ ):
229
+ competitors.add(candidate)
230
+
231
+ # Capitalised word BEFORE keyword: "Amplitude is an alternative"
232
+ before_pattern = (
233
+ r"([A-Z][a-zA-Z]+)\s+(?:\w+\s+)*?"
234
+ r"(?i:" + re.escape(keyword) + r")"
235
+ )
236
+ for m in re.finditer(before_pattern, content):
237
+ candidate = m.group(1).strip()
238
+ if (
239
+ len(candidate) > 1
240
+ and candidate.lower() not in STOP_WORDS
241
+ and candidate[0].isupper()
242
+ ):
243
+ competitors.add(candidate)
244
+
245
+ return sorted(competitors)
246
+
247
+ # ------------------------------------------------------------------
248
+ # Upstream context extraction
249
+ # ------------------------------------------------------------------
250
+
251
+ def _extract_upstream_context(
252
+ self,
253
+ context: dict[str, Any] | None,
254
+ ) -> dict[str, Any]:
255
+ """Extract structured upstream context from SharedContext.
256
+
257
+ Looks for:
258
+ - echo_social.top_mentions — social chatter about competitors
259
+ - sage_triage.issues — community-reported issues (churn signals, comparisons)
260
+ """
261
+ extracted: dict[str, Any] = {
262
+ "social_mentions": [],
263
+ "community_issues": [],
264
+ }
265
+ if not context:
266
+ return extracted
267
+
268
+ # Echo social mentions
269
+ if "echo_social" in context:
270
+ echo = context["echo_social"]
271
+ if isinstance(echo, dict):
272
+ for mention in echo.get("top_mentions", []):
273
+ if isinstance(mention, dict):
274
+ extracted["social_mentions"].append(
275
+ {
276
+ "platform": mention.get("platform", ""),
277
+ "title": mention.get("title", ""),
278
+ "sentiment": mention.get("sentiment", ""),
279
+ "url": mention.get("url", ""),
280
+ }
281
+ )
282
+
283
+ # Sage triage issues
284
+ if "sage_triage" in context:
285
+ sage = context["sage_triage"]
286
+ if isinstance(sage, dict):
287
+ for issue in sage.get("issues", [])[:10]:
288
+ if isinstance(issue, dict):
289
+ extracted["community_issues"].append(
290
+ {
291
+ "number": issue.get("number"),
292
+ "title": issue.get("title", ""),
293
+ "category": issue.get("category", ""),
294
+ "product_area": issue.get("product_area", ""),
295
+ }
296
+ )
297
+
298
+ return extracted
299
+
300
+ # ------------------------------------------------------------------
301
+ # Apollo enrichment
302
+ # ------------------------------------------------------------------
303
+
304
+ async def enrich_competitor_profile(
305
+ self,
306
+ name: str,
307
+ domain: str,
308
+ ) -> dict[str, Any] | None:
309
+ """Enrich a competitor with Apollo org data."""
310
+ if not self.apollo_client:
311
+ return None
312
+ try:
313
+ org = await self.apollo_client.enrich_organization(domain=domain)
314
+ except Exception as exc:
315
+ logger.warning(f"Apollo enrichment failed for {domain}: {exc}")
316
+ return None
317
+ if not org:
318
+ return None
319
+ return {
320
+ "name": name,
321
+ "domain": domain,
322
+ "tech_stack": org.tech_stack,
323
+ "estimated_headcount": org.estimated_headcount,
324
+ "funding_stage": org.funding_stage,
325
+ "funding_total": org.funding_total,
326
+ "industry": org.industry,
327
+ }
328
+
329
+ # ------------------------------------------------------------------
330
+ # Execute
331
+ # ------------------------------------------------------------------
332
+
333
+ async def execute(
334
+ self,
335
+ task: str,
336
+ context: Optional[dict[str, Any]] = None,
337
+ ) -> dict[str, Any]:
338
+ """
339
+ Execute a competitive intelligence task.
340
+
341
+ Steps:
342
+ 1. Discover competitors from task string and KB
343
+ 2. Web-search each competitor for recent activity
344
+ 3. Search KB for competitive context
345
+ 4. Build LLM prompt with all gathered data
346
+ 5. Generate structured competitive report via LLM
347
+
348
+ Degrades gracefully:
349
+ - Without search_tools: skips web search
350
+ - Without llm_client: returns prompt_used instead of content
351
+ """
352
+ logger.info(f"Rex executing: {task[:80]}...")
353
+
354
+ # 1. Discover competitors
355
+ competitors = self._discover_competitors(task)
356
+ logger.info(f"Discovered competitors: {competitors}")
357
+
358
+ # 2. Web search per competitor (semaphore-bounded parallel fan-out)
359
+ web_intel: dict[str, list[dict[str, str]]] = {}
360
+ if self.search_tools and competitors:
361
+ search_sem = asyncio.Semaphore(SEARCH_CONCURRENCY)
362
+
363
+ async def _search_competitor(comp: str) -> tuple[str, list[dict[str, str]]]:
364
+ async with search_sem:
365
+ try:
366
+ results = await self.search_tools.web_search(
367
+ f"{comp} vs {self.product_name}",
368
+ limit=5,
369
+ )
370
+ return comp, [
371
+ {"title": r.title, "url": r.url, "snippet": r.snippet} for r in results
372
+ ]
373
+ except Exception as exc:
374
+ logger.warning(f"Web search failed for {comp}: {exc}")
375
+ return comp, []
376
+
377
+ search_results = await asyncio.gather(*[_search_competitor(c) for c in competitors])
378
+ web_intel = dict(search_results)
379
+
380
+ # 2b. Apollo enrichment per competitor (semaphore-bounded parallel)
381
+ enriched_profiles: list[dict[str, Any]] = []
382
+ if self.apollo_client and competitors:
383
+ enrich_sem = asyncio.Semaphore(SEARCH_CONCURRENCY)
384
+
385
+ async def _enrich(comp: str) -> dict[str, Any] | None:
386
+ async with enrich_sem:
387
+ return await self.enrich_competitor_profile(comp, _guess_domain(comp))
388
+
389
+ enrichment_results = await asyncio.gather(*[_enrich(c) for c in competitors])
390
+ enriched_profiles = [p for p in enrichment_results if p]
391
+
392
+ # 3. Search KB for competitive context
393
+ kb_docs = self._kb.search(task)
394
+ kb_context = "\n\n".join(f"[Source: {doc['source']}]\n{doc['content']}" for doc in kb_docs)
395
+
396
+ # 4. Extract upstream context
397
+ upstream = self._extract_upstream_context(context)
398
+
399
+ # 5. Build prompt
400
+ web_section = ""
401
+ if web_intel:
402
+ web_section = "## Web Intelligence\n"
403
+ for comp, results in web_intel.items():
404
+ web_section += f"\n### {comp}\n"
405
+ for r in results:
406
+ web_section += f"- [{r['title']}]({r['url']}): {r['snippet']}\n"
407
+
408
+ social_section = ""
409
+ if upstream["social_mentions"]:
410
+ social_section = "## Social Mentions (from Echo)\n"
411
+ for m in upstream["social_mentions"]:
412
+ social_section += (
413
+ f"- [{m['platform']}] {m['title']} (sentiment: {m['sentiment']})\n"
414
+ )
415
+
416
+ issues_section = ""
417
+ if upstream["community_issues"]:
418
+ issues_section = "## Community Issues (from Sage)\n"
419
+ for issue in upstream["community_issues"]:
420
+ issues_section += (
421
+ f"- #{issue['number']}: {issue['title']} [{issue.get('product_area', '')}]\n"
422
+ )
423
+
424
+ enriched_section = ""
425
+ if enriched_profiles:
426
+ enriched_section = "## Apollo Firmographic Data\n"
427
+ for p in enriched_profiles:
428
+ enriched_section += (
429
+ f"- {p['name']} ({p['domain']}): "
430
+ f"headcount={p.get('estimated_headcount', '?')}, "
431
+ f"funding={p.get('funding_stage', '?')}, "
432
+ f"tech={p.get('tech_stack', [])}\n"
433
+ )
434
+
435
+ user_prompt = f"""Task: {task}
436
+
437
+ ## Competitors Identified
438
+ {", ".join(competitors) if competitors else "No competitors identified yet."}
439
+
440
+ ## Knowledge Base
441
+ {kb_context if kb_context else "No relevant knowledge base documents found."}
442
+
443
+ {web_section}
444
+
445
+ {social_section}
446
+
447
+ {issues_section}
448
+
449
+ {enriched_section}
450
+
451
+ ## Instructions
452
+ Produce a competitive intelligence report in JSON with this structure:
453
+ {{
454
+ "summary": "executive summary",
455
+ "competitors": [
456
+ {{
457
+ "name": "...",
458
+ "category": "direct|indirect|emerging",
459
+ "strengths": ["..."],
460
+ "weaknesses": ["..."],
461
+ "recent_moves": ["..."],
462
+ "market_position": "..."
463
+ }}
464
+ ],
465
+ "threats": [
466
+ {{
467
+ "source": "...",
468
+ "description": "...",
469
+ "severity": "low|medium|high|critical",
470
+ "timeframe": "immediate|short-term|long-term",
471
+ "recommended_response": "..."
472
+ }}
473
+ ],
474
+ "opportunities": [
475
+ {{
476
+ "description": "...",
477
+ "source": "...",
478
+ "impact": "low|medium|high",
479
+ "effort": "low|medium|high",
480
+ "recommended_action": "..."
481
+ }}
482
+ ]
483
+ }}
484
+
485
+ Ground all analysis in the evidence provided above. Do not speculate.
486
+ Return ONLY the JSON object.
487
+ """
488
+
489
+ system_prompt = self._system_prompt
490
+
491
+ base_result: dict[str, Any] = {
492
+ "agent": "rex",
493
+ "task": task,
494
+ "competitors_discovered": competitors,
495
+ "web_intel_sources": {comp: len(results) for comp, results in web_intel.items()},
496
+ "kb_sources": [doc["source"] for doc in kb_docs],
497
+ "upstream_social_mentions": len(upstream["social_mentions"]),
498
+ "upstream_community_issues": len(upstream["community_issues"]),
499
+ }
500
+
501
+ base_result["enriched_profiles"] = enriched_profiles
502
+
503
+ if self.llm_client:
504
+ try:
505
+ raw = await self.llm_client.generate(
506
+ system_prompt=system_prompt,
507
+ user_prompt=user_prompt,
508
+ temperature=0.3,
509
+ max_tokens=4096,
510
+ )
511
+ cleaned = strip_markdown_fences(raw)
512
+ try:
513
+ parsed = json.loads(cleaned)
514
+ base_result["content"] = parsed
515
+ base_result["status"] = "generated"
516
+ except json.JSONDecodeError as e:
517
+ logger.warning(f"Rex JSON parse failed: {e}")
518
+ logger.debug(f"Rex raw response head: {cleaned[:500]}")
519
+ base_result["status"] = "parse_error"
520
+ base_result["raw_content"] = cleaned
521
+ base_result["content"] = {}
522
+ base_result["error"] = f"JSON parse failed: {e}"
523
+ except Exception as exc:
524
+ logger.warning(f"LLM generation failed: {exc}")
525
+ base_result["status"] = "error"
526
+ base_result["error"] = str(exc)
527
+ base_result["prompt_used"] = user_prompt[:500]
528
+ else:
529
+ base_result["status"] = "generated"
530
+ base_result["prompt_used"] = user_prompt[:500]
531
+
532
+ return base_result