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
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2Unified Agent Goal Engine - API Blueprint
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
10from integrations.social.auth import require_auth, require_admin
11from core.auth_local import require_local_or_token
13logger = logging.getLogger('hevolve_social')
15agent_engine_bp = Blueprint('agent_engine', __name__)
18# ─── Products ───
20@agent_engine_bp.route('/api/marketing/products', methods=['POST'])
21@require_auth
22def create_product():
23 from .goal_manager import ProductManager
25 data = request.get_json() or {}
26 if not data.get('name'):
27 return jsonify({'success': False, 'error': 'name is required'}), 400
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
46@agent_engine_bp.route('/api/marketing/products', methods=['GET'])
47@require_auth
48def list_products():
49 from .goal_manager import ProductManager
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})
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))
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
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
76 data = request.get_json() or {}
77 result = ProductManager.update_product(g.db, product_id, **data)
78 return jsonify(result)
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
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
93 return jsonify(ProductManager.delete_product(g.db, product_id))
96# ─── Goals (unified — any goal_type) ───
98@agent_engine_bp.route('/api/goals', methods=['POST'])
99@require_auth
100def create_goal():
101 from .goal_manager import GoalManager, get_registered_types
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
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
127@agent_engine_bp.route('/api/goals', methods=['GET'])
128@require_auth
129def list_goals():
130 from .goal_manager import GoalManager
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})
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))
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
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
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))
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
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
178 return jsonify(GoalManager.update_goal_status(g.db, goal_id, 'archived'))
181# ─── Speculative Execution ───
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))
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 })
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 })
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.
246import re as _re
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)
265def _iter_ledgers(agent_filter=None):
266 """Yield ``(agent_id, session_id, SmartLedger)`` for every JSON
267 ledger file in ``get_agent_data_dir()``.
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
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
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.
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
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
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
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
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
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.
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
425# ─── IP Protection ───
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})
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
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})
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})
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})
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
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()})
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})
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()})
540# ─── Defensive Publications ───
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})
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
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)})
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})
588# ─── World Model Health ───
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 })