Coverage for integrations / agent_engine / api.py: 52.5%

299 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1""" 

2Unified Agent Goal Engine - API Blueprint 

3 

4Unified endpoints for products + goals of any type. 

510 endpoints total (5 product + 5 goal). 

6""" 

7import logging 

8from flask import Blueprint, request, jsonify, g 

9 

10from integrations.social.auth import require_auth, require_admin 

11from core.auth_local import require_local_or_token 

12 

13logger = logging.getLogger('hevolve_social') 

14 

15agent_engine_bp = Blueprint('agent_engine', __name__) 

16 

17 

18# ─── Products ─── 

19 

20@agent_engine_bp.route('/api/marketing/products', methods=['POST']) 

21@require_auth 

22def create_product(): 

23 from .goal_manager import ProductManager 

24 

25 data = request.get_json() or {} 

26 if not data.get('name'): 

27 return jsonify({'success': False, 'error': 'name is required'}), 400 

28 

29 result = ProductManager.create_product( 

30 g.db, 

31 name=data['name'], 

32 owner_id=str(g.user.id), 

33 description=data.get('description', ''), 

34 tagline=data.get('tagline', ''), 

35 product_url=data.get('product_url', ''), 

36 logo_url=data.get('logo_url', ''), 

37 category=data.get('category', 'general'), 

38 target_audience=data.get('target_audience', ''), 

39 unique_value_prop=data.get('unique_value_prop', ''), 

40 keywords=data.get('keywords', []), 

41 is_platform_product=data.get('is_platform_product', False), 

42 ) 

43 return jsonify(result), 201 if result.get('success') else 400 

44 

45 

46@agent_engine_bp.route('/api/marketing/products', methods=['GET']) 

47@require_auth 

48def list_products(): 

49 from .goal_manager import ProductManager 

50 

51 owner_id = request.args.get('owner_id', str(g.user.id)) 

52 status = request.args.get('status') 

53 products = ProductManager.list_products(g.db, owner_id=owner_id, status=status) 

54 return jsonify({'success': True, 'products': products}) 

55 

56 

57@agent_engine_bp.route('/api/marketing/products/<product_id>', methods=['GET']) 

58@require_auth 

59def get_product(product_id): 

60 from .goal_manager import ProductManager 

61 return jsonify(ProductManager.get_product(g.db, product_id)) 

62 

63 

64@agent_engine_bp.route('/api/marketing/products/<product_id>', methods=['PUT']) 

65@require_auth 

66def update_product(product_id): 

67 from .goal_manager import ProductManager 

68 from integrations.social.models import Product 

69 

70 product = g.db.query(Product).filter_by(id=product_id).first() 

71 if not product: 

72 return jsonify({'success': False, 'error': 'Product not found'}), 404 

73 if str(product.owner_id) != str(g.user.id): 

74 return jsonify({'success': False, 'error': 'Not authorized'}), 403 

75 

76 data = request.get_json() or {} 

77 result = ProductManager.update_product(g.db, product_id, **data) 

78 return jsonify(result) 

79 

80 

81@agent_engine_bp.route('/api/marketing/products/<product_id>', methods=['DELETE']) 

82@require_auth 

83def delete_product(product_id): 

84 from .goal_manager import ProductManager 

85 from integrations.social.models import Product 

86 

87 product = g.db.query(Product).filter_by(id=product_id).first() 

88 if not product: 

89 return jsonify({'success': False, 'error': 'Product not found'}), 404 

90 if str(product.owner_id) != str(g.user.id): 

91 return jsonify({'success': False, 'error': 'Not authorized'}), 403 

92 

93 return jsonify(ProductManager.delete_product(g.db, product_id)) 

94 

95 

96# ─── Goals (unified — any goal_type) ─── 

97 

98@agent_engine_bp.route('/api/goals', methods=['POST']) 

99@require_auth 

100def create_goal(): 

101 from .goal_manager import GoalManager, get_registered_types 

102 

103 data = request.get_json() or {} 

104 goal_type = data.get('goal_type', '') 

105 if not goal_type: 

106 return jsonify({'success': False, 'error': 'goal_type is required'}), 400 

107 if goal_type not in get_registered_types(): 

108 return jsonify({'success': False, 

109 'error': f'Unknown goal_type: {goal_type}. ' 

110 f'Available: {get_registered_types()}'}), 400 

111 if not data.get('title'): 

112 return jsonify({'success': False, 'error': 'title is required'}), 400 

113 

114 result = GoalManager.create_goal( 

115 g.db, 

116 goal_type=goal_type, 

117 title=data['title'], 

118 description=data.get('description', ''), 

119 config=data.get('config', {}), 

120 product_id=data.get('product_id'), 

121 spark_budget=data.get('spark_budget', 200), 

122 created_by=str(g.user.id), 

123 ) 

124 return jsonify(result), 201 if result.get('success') else 400 

125 

126 

127@agent_engine_bp.route('/api/goals', methods=['GET']) 

128@require_auth 

129def list_goals(): 

130 from .goal_manager import GoalManager 

131 

132 goal_type = request.args.get('goal_type') 

133 status = request.args.get('status') 

134 product_id = request.args.get('product_id') 

135 goals = GoalManager.list_goals(g.db, goal_type=goal_type, 

136 status=status, product_id=product_id) 

137 return jsonify({'success': True, 'goals': goals}) 

138 

139 

140@agent_engine_bp.route('/api/goals/<goal_id>', methods=['GET']) 

141@require_auth 

142def get_goal(goal_id): 

143 from .goal_manager import GoalManager 

144 return jsonify(GoalManager.get_goal(g.db, goal_id)) 

145 

146 

147@agent_engine_bp.route('/api/goals/<goal_id>/status', methods=['PATCH']) 

148@require_auth 

149def update_goal_status(goal_id): 

150 from .goal_manager import GoalManager 

151 from integrations.social.models import AgentGoal 

152 

153 goal = g.db.query(AgentGoal).filter_by(id=goal_id).first() 

154 if not goal: 

155 return jsonify({'success': False, 'error': 'Goal not found'}), 404 

156 if goal.created_by and str(goal.created_by) != str(g.user.id): 

157 return jsonify({'success': False, 'error': 'Not authorized'}), 403 

158 

159 data = request.get_json() or {} 

160 status = data.get('status') 

161 if not status: 

162 return jsonify({'success': False, 'error': 'status is required'}), 400 

163 return jsonify(GoalManager.update_goal_status(g.db, goal_id, status)) 

164 

165 

166@agent_engine_bp.route('/api/goals/<goal_id>', methods=['DELETE']) 

167@require_auth 

168def delete_goal(goal_id): 

169 from .goal_manager import GoalManager 

170 from integrations.social.models import AgentGoal 

171 

172 goal = g.db.query(AgentGoal).filter_by(id=goal_id).first() 

173 if not goal: 

174 return jsonify({'success': False, 'error': 'Goal not found'}), 404 

175 if goal.created_by and str(goal.created_by) != str(g.user.id): 

176 return jsonify({'success': False, 'error': 'Not authorized'}), 403 

177 

178 return jsonify(GoalManager.update_goal_status(g.db, goal_id, 'archived')) 

179 

180 

181# ─── Speculative Execution ─── 

182 

183@agent_engine_bp.route('/api/agent-engine/speculation/<speculation_id>', methods=['GET']) 

184@require_auth 

185def get_speculation_status(speculation_id): 

186 """Get the status of a speculative dispatch (expert background task).""" 

187 from .speculative_dispatcher import get_speculative_dispatcher 

188 dispatcher = get_speculative_dispatcher() 

189 return jsonify(dispatcher.get_speculation_status(speculation_id)) 

190 

191 

192@agent_engine_bp.route('/api/agent-engine/stats', methods=['GET']) 

193@require_auth 

194def get_engine_stats(): 

195 """Get agent engine stats: active speculations, energy consumed, models.""" 

196 from .speculative_dispatcher import get_speculative_dispatcher 

197 from .model_registry import model_registry 

198 dispatcher = get_speculative_dispatcher() 

199 return jsonify({ 

200 'success': True, 

201 'speculation': dispatcher.get_stats(), 

202 'models': [m.to_dict() for m in model_registry.list_models()], 

203 }) 

204 

205 

206@agent_engine_bp.route('/api/agent-engine/guardrails', methods=['GET']) 

207@require_auth 

208def get_guardrail_status(): 

209 """Get guardrail system status.""" 

210 from security.hive_guardrails import ( 

211 HiveCircuitBreaker, CONSTITUTIONAL_RULES, COMPUTE_CAPS, 

212 WORLD_MODEL_BOUNDS, 

213 ) 

214 return jsonify({ 

215 'success': True, 

216 'circuit_breaker': HiveCircuitBreaker.get_status(), 

217 'constitutional_rules_count': len(CONSTITUTIONAL_RULES), 

218 'compute_caps': COMPUTE_CAPS, 

219 'world_model_bounds': WORLD_MODEL_BOUNDS, 

220 }) 

221 

222 

223# ─── Agent Ledger ─── 

224# 

225# SmartLedger is per-(agent_id, session_id): each agent goal creates 

226# its own ledger file at ``<get_agent_data_dir()>/ledger_<agent>_<session>.json`` 

227# (see ``agent_ledger.core.SmartLedger.__init__``). The admin Task 

228# Ledger view wants the UNION across all of them, so these handlers 

229# walk the storage dir and aggregate in-memory. 

230# 

231# Original T18 commit (4e4554e) used ``SmartLedger.get_instance()`` / 

232# ``ledger.list_tasks()`` / ``ledger.get_stats()`` — none of which 

233# exist on SmartLedger; the route family never worked. Rewritten to 

234# use the actual public API: ``ledger.tasks``, ``ledger.get_task``, 

235# ``ledger.get_progress_summary``. 

236# 

237# JSON-backend only. When a deployment switches to RedisBackend, the 

238# filesystem walk misses Redis-resident tasks; that path needs a 

239# separate Redis SCAN-based aggregator (TODO). 

240# 

241# Filename pattern is strict UUID_UUID to reject path-traversal 

242# attempts (``ledger_..%2F..%2Fetc.json``) and also to safely skip 

243# sibling files like ``benchmark_ledger.json`` that share the 

244# directory. 

245 

246import re as _re 

247 

248# Real on-disk session_id formats observed in production 

249# (see ~/Documents/Nunba/data/agent_data/): 

250# ledger_<uuid>_<uuid>.json — encounter / icebreaker agents 

251# ledger_<uuid>_<numeric>.json — autoresearch / langchain sessions 

252# ledger_<uuid>_<arbitrary>.json — possible future formats 

253# So agent_id is anchored to strict UUID (every visible filename has one), 

254# but session_id is any safe identifier — alphanumeric / hyphen / 

255# underscore. Anchored ^...$ + restricted charset prevents both 

256# path traversal (no ``..%2F``, no ``/``) and matches against sibling 

257# files like ``benchmark_ledger.json`` that share the directory. 

258_LEDGER_UUID_RE = (r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}' 

259 r'-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') 

260_LEDGER_FILENAME = _re.compile( 

261 rf'^ledger_(?P<agent>{_LEDGER_UUID_RE})_(?P<session>[A-Za-z0-9_\-]+)\.json$' 

262) 

263 

264 

265def _iter_ledgers(agent_filter=None): 

266 """Yield ``(agent_id, session_id, SmartLedger)`` for every JSON 

267 ledger file in ``get_agent_data_dir()``. 

268 

269 Honours the per-(agent, session) cache from 

270 ``agent_ledger.factory.get_or_create_ledger`` so repeat polls 

271 don't re-parse JSON each time. ``prefer_redis=False`` because 

272 we are explicitly walking on-disk JSON files; a RedisBackend 

273 instance would not surface them via ``Path.glob``. 

274 """ 

275 from agent_ledger.factory import get_or_create_ledger 

276 from core.platform_paths import get_agent_data_dir 

277 from pathlib import Path 

278 

279 storage_dir = Path(get_agent_data_dir()) 

280 if not storage_dir.is_dir(): 

281 return 

282 for ledger_file in storage_dir.glob('ledger_*.json'): 

283 m = _LEDGER_FILENAME.match(ledger_file.name) 

284 if not m: 

285 # benchmark_ledger.json, malformed names, traversal attempts. 

286 continue 

287 agent_id, session_id = m.group('agent'), m.group('session') 

288 if agent_filter and agent_id != agent_filter: 

289 continue 

290 try: 

291 ledger = get_or_create_ledger( 

292 agent_id=agent_id, 

293 session_id=session_id, 

294 use_cache=True, 

295 storage_dir=str(storage_dir), 

296 prefer_redis=False, 

297 ) 

298 yield agent_id, session_id, ledger 

299 except Exception as e: 

300 logger.warning(f"Skipped ledger {ledger_file.name}: {e}") 

301 continue 

302 

303 

304@agent_engine_bp.route('/api/agent-engine/ledger/tasks', methods=['GET']) 

305@require_local_or_token 

306def list_ledger_tasks(): 

307 """List tasks aggregated across all per-session SmartLedgers. 

308 

309 Query params: 

310 ``status`` — TaskStatus value (e.g. ``in_progress``, ``completed``) 

311 ``agent_id`` — filter to one agent's ledgers (UUID) 

312 ``limit`` — max tasks returned (1..1000, default 50) 

313 """ 

314 try: 

315 from agent_ledger import TaskStatus 

316 except ImportError: 

317 return jsonify({'success': False, 

318 'error': 'agent_ledger not installed'}), 501 

319 try: 

320 status_filter = request.args.get('status') 

321 agent_filter = request.args.get('agent_id') 

322 try: 

323 limit = max(1, min(int(request.args.get('limit', 50)), 1000)) 

324 except (TypeError, ValueError): 

325 limit = 50 

326 

327 status_enum = None 

328 if status_filter: 

329 try: 

330 status_enum = TaskStatus(status_filter) 

331 except ValueError: 

332 return jsonify({'success': False, 

333 'error': f'Unknown status: {status_filter}'}), 400 

334 

335 all_tasks = [] 

336 for agent_id, session_id, ledger in _iter_ledgers(agent_filter): 

337 for task in ledger.tasks.values(): 

338 if status_enum is not None and task.status != status_enum: 

339 continue 

340 all_tasks.append({ 

341 **task.to_dict(), 

342 'agent_id': agent_id, 

343 'session_id': session_id, 

344 }) 

345 if len(all_tasks) >= limit: 

346 break 

347 if len(all_tasks) >= limit: 

348 break 

349 

350 return jsonify({ 

351 'success': True, 

352 'tasks': all_tasks, 

353 'total': len(all_tasks), 

354 }) 

355 except Exception: 

356 logger.exception("list_ledger_tasks failed") 

357 return jsonify({'success': False, 

358 'error': 'Internal server error'}), 500 

359 

360 

361@agent_engine_bp.route('/api/agent-engine/ledger/tasks/<task_id>', methods=['GET']) 

362@require_local_or_token 

363def get_ledger_task(task_id): 

364 """Get a single task by ID, searching across all per-session ledgers.""" 

365 try: 

366 for agent_id, session_id, ledger in _iter_ledgers(): 

367 task = ledger.get_task(task_id) 

368 if task is not None: 

369 return jsonify({ 

370 'success': True, 

371 'task': { 

372 **task.to_dict(), 

373 'agent_id': agent_id, 

374 'session_id': session_id, 

375 }, 

376 }) 

377 return jsonify({'success': False, 'error': 'Task not found'}), 404 

378 except ImportError: 

379 return jsonify({'success': False, 

380 'error': 'agent_ledger not installed'}), 501 

381 except Exception: 

382 logger.exception("get_ledger_task failed") 

383 return jsonify({'success': False, 

384 'error': 'Internal server error'}), 500 

385 

386 

387@agent_engine_bp.route('/api/agent-engine/ledger/stats', methods=['GET']) 

388@require_local_or_token 

389def get_ledger_stats(): 

390 """Aggregate ledger stats across all per-session SmartLedgers. 

391 

392 Sums per-status counts and totals over every JSON ledger in 

393 ``get_agent_data_dir()``. ``by_status`` keys are TaskStatus 

394 string values (``pending``, ``in_progress``, ...) — the dict 

395 returned by ``SmartLedger.get_task_state_summary`` uses enum 

396 keys, so we coerce to ``.value`` for JSON serialization. 

397 """ 

398 try: 

399 total = 0 

400 sessions = 0 

401 by_status = {} 

402 for _agent_id, _session_id, ledger in _iter_ledgers(): 

403 sessions += 1 

404 for task in ledger.tasks.values(): 

405 total += 1 

406 key = task.status.value if hasattr(task.status, 'value') else str(task.status) 

407 by_status[key] = by_status.get(key, 0) + 1 

408 return jsonify({ 

409 'success': True, 

410 'stats': { 

411 'total': total, 

412 'sessions': sessions, 

413 'by_status': by_status, 

414 }, 

415 }) 

416 except ImportError: 

417 return jsonify({'success': False, 

418 'error': 'agent_ledger not installed'}), 501 

419 except Exception: 

420 logger.exception("get_ledger_stats failed") 

421 return jsonify({'success': False, 

422 'error': 'Internal server error'}), 500 

423 

424 

425# ─── IP Protection ─── 

426 

427@agent_engine_bp.route('/api/ip/patents', methods=['GET']) 

428@require_auth 

429def list_patents(): 

430 from .ip_service import IPService 

431 status = request.args.get('status') 

432 patents = IPService.list_patents(g.db, status=status) 

433 return jsonify({'success': True, 'patents': patents}) 

434 

435 

436@agent_engine_bp.route('/api/ip/patents', methods=['POST']) 

437@require_auth 

438def create_patent(): 

439 from .ip_service import IPService 

440 data = request.get_json() or {} 

441 if not data.get('title'): 

442 return jsonify({'success': False, 'error': 'title is required'}), 400 

443 result = IPService.create_patent( 

444 g.db, 

445 title=data['title'], 

446 claims=data.get('claims', []), 

447 abstract=data.get('abstract', ''), 

448 description=data.get('description', ''), 

449 filing_type=data.get('filing_type', 'provisional'), 

450 verification_metrics=data.get('verification_metrics'), 

451 evidence=data.get('evidence'), 

452 goal_id=data.get('goal_id'), 

453 created_by=str(g.user.id), 

454 ) 

455 return jsonify({'success': True, 'patent': result}), 201 

456 

457 

458@agent_engine_bp.route('/api/ip/patents/<patent_id>', methods=['GET']) 

459@require_auth 

460def get_patent(patent_id): 

461 from .ip_service import IPService 

462 result = IPService.get_patent(g.db, patent_id) 

463 if not result: 

464 return jsonify({'success': False, 'error': 'Patent not found'}), 404 

465 return jsonify({'success': True, 'patent': result}) 

466 

467 

468@agent_engine_bp.route('/api/ip/patents/<patent_id>/status', methods=['PATCH']) 

469@require_auth 

470def update_patent_status(patent_id): 

471 from .ip_service import IPService 

472 data = request.get_json() or {} 

473 status = data.get('status') 

474 if not status: 

475 return jsonify({'success': False, 'error': 'status is required'}), 400 

476 result = IPService.update_patent_status( 

477 g.db, patent_id, status, 

478 application_number=data.get('application_number'), 

479 patent_number=data.get('patent_number'), 

480 ) 

481 if not result: 

482 return jsonify({'success': False, 'error': 'Patent not found'}), 404 

483 return jsonify({'success': True, 'patent': result}) 

484 

485 

486@agent_engine_bp.route('/api/ip/infringements', methods=['GET']) 

487@require_auth 

488def list_infringements(): 

489 from .ip_service import IPService 

490 patent_id = request.args.get('patent_id') 

491 status = request.args.get('status') 

492 infringements = IPService.list_infringements(g.db, patent_id=patent_id, status=status) 

493 return jsonify({'success': True, 'infringements': infringements}) 

494 

495 

496@agent_engine_bp.route('/api/ip/infringements', methods=['POST']) 

497@require_auth 

498def create_infringement(): 

499 from .ip_service import IPService 

500 data = request.get_json() or {} 

501 if not data.get('infringer_name'): 

502 return jsonify({'success': False, 'error': 'infringer_name is required'}), 400 

503 result = IPService.create_infringement( 

504 g.db, 

505 patent_id=data.get('patent_id', ''), 

506 infringer_name=data['infringer_name'], 

507 infringer_url=data.get('infringer_url', ''), 

508 evidence_summary=data.get('evidence_summary', ''), 

509 risk_level=data.get('risk_level', 'low'), 

510 ) 

511 return jsonify({'success': True, 'infringement': result}), 201 

512 

513 

514@agent_engine_bp.route('/api/ip/loop-health', methods=['GET']) 

515@require_auth 

516def get_loop_health(): 

517 """Self-improving loop dashboard — flywheel health + detected loopholes.""" 

518 from .ip_service import IPService 

519 return jsonify({'success': True, 'data': IPService.get_loop_health()}) 

520 

521 

522@agent_engine_bp.route('/api/ip/verify', methods=['GET']) 

523@require_auth 

524def verify_loop(): 

525 """Verify exponential improvement — gates patent filing.""" 

526 from .ip_service import IPService 

527 days = request.args.get('days', 30, type=int) 

528 result = IPService.verify_exponential_improvement(g.db, days=days) 

529 return jsonify({'success': True, 'data': result}) 

530 

531 

532@agent_engine_bp.route('/api/ip/moat', methods=['GET']) 

533@require_auth 

534def get_moat_depth(): 

535 """Technical irreproducibility — how far ahead of a code clone.""" 

536 from .ip_service import IPService 

537 return jsonify({'success': True, 'data': IPService.measure_moat_depth()}) 

538 

539 

540# ─── Defensive Publications ─── 

541 

542@agent_engine_bp.route('/api/ip/defensive-publications', methods=['GET']) 

543@require_auth 

544def list_defensive_publications(): 

545 """List all defensive publications — timestamped prior art evidence.""" 

546 from .ip_service import IPService 

547 pubs = IPService.list_defensive_publications(g.db) 

548 return jsonify({'success': True, 'publications': pubs}) 

549 

550 

551@agent_engine_bp.route('/api/ip/defensive-publications', methods=['POST']) 

552@require_auth 

553def create_defensive_publication(): 

554 """Create a new defensive publication — signed prior art proof.""" 

555 from .ip_service import IPService 

556 data = request.get_json() or {} 

557 if not data.get('title') or not data.get('content'): 

558 return jsonify({'success': False, 'error': 'title and content required'}), 400 

559 result = IPService.create_defensive_publication( 

560 g.db, 

561 title=data['title'], 

562 content=data['content'], 

563 abstract=data.get('abstract', ''), 

564 git_commit=data.get('git_commit'), 

565 created_by=str(g.user.id), 

566 ) 

567 return jsonify({'success': True, 'publication': result}), 201 

568 

569 

570@agent_engine_bp.route('/api/ip/provenance', methods=['GET']) 

571@require_auth 

572def get_provenance(): 

573 """Full provenance chain — all publications, patents, moat, evidence.""" 

574 from .ip_service import IPService 

575 return jsonify({'success': True, 'data': IPService.get_provenance_record(g.db)}) 

576 

577 

578@agent_engine_bp.route('/api/ip/milestone', methods=['GET']) 

579@require_auth 

580def check_milestone(): 

581 """Check intelligence milestone — auto-patent filing trigger status.""" 

582 from .ip_service import IPService 

583 days = request.args.get('days', 14, type=int) 

584 result = IPService.check_intelligence_milestone(g.db, consecutive_days_required=days) 

585 return jsonify({'success': True, 'data': result}) 

586 

587 

588# ─── World Model Health ─── 

589 

590@agent_engine_bp.route('/api/world-model/health', methods=['GET']) 

591def world_model_health(): 

592 """World model bridge health check — no auth required for monitoring.""" 

593 try: 

594 from .world_model_bridge import get_world_model_bridge 

595 bridge = get_world_model_bridge() 

596 return jsonify({ 

597 'success': True, 

598 'health': bridge.check_health(), 

599 'stats': bridge.get_learning_stats(), 

600 }) 

601 except Exception as e: 

602 return jsonify({ 

603 'success': False, 

604 'health': {'healthy': False, 'error': str(e)}, 

605 })