Initial commit: add .gitignore and README
Some checks failed
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lint (push) Has been cancelled
Tests / docker (push) Has been cancelled

This commit is contained in:
defiQUG
2026-02-09 21:51:42 -08:00
commit c052b07662
3146 changed files with 808305 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
"""API routes for Dvādaśa sessions and prompts."""
from fastapi import APIRouter
from fusionagi.api.routes.sessions import router as sessions_router
from fusionagi.api.routes.tts import router as tts_router
from fusionagi.api.routes.admin import router as admin_router
from fusionagi.api.routes.openai_compat import router as openai_compat_router
router = APIRouter()
router.include_router(sessions_router, prefix="/sessions", tags=["sessions"])
router.include_router(tts_router, prefix="/sessions", tags=["tts"])
router.include_router(admin_router, prefix="/admin", tags=["admin"])
router.include_router(openai_compat_router)

View File

@@ -0,0 +1,17 @@
"""Admin routes: telemetry, etc."""
from fastapi import APIRouter
from fusionagi.api.dependencies import get_telemetry_tracer
router = APIRouter()
@router.get("/telemetry")
def get_telemetry(task_id: str | None = None, limit: int = 100) -> dict:
"""Return telemetry traces (admin). Filter by task_id if provided."""
tracer = get_telemetry_tracer()
if not tracer:
return {"traces": []}
traces = tracer.get_traces(task_id=task_id, limit=limit)
return {"traces": traces}

View File

@@ -0,0 +1,265 @@
"""OpenAI-compatible API routes for Cursor Composer and other consumers."""
import asyncio
import json
import uuid
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from fastapi import APIRouter, Depends, Header, HTTPException, Request
from starlette.responses import StreamingResponse
from fusionagi.api.dependencies import (
ensure_initialized,
get_event_bus,
get_orchestrator,
get_safety_pipeline,
get_openai_bridge_config,
verify_openai_bridge_auth,
)
from fusionagi.api.openai_compat.translators import (
messages_to_prompt,
final_response_to_openai,
estimate_usage,
)
from fusionagi.core import run_dvadasa
from fusionagi.schemas.commands import parse_user_input
router = APIRouter(tags=["openai-compat"])
# Chunk size for streaming (chars per SSE delta)
_STREAM_CHUNK_SIZE = 50
def _openai_error(status_code: int, message: str, error_type: str) -> HTTPException:
"""Raise HTTPException with OpenAI-style error body."""
return HTTPException(
status_code=status_code,
detail={"error": {"message": message, "type": error_type}},
)
def _ensure_openai_init() -> None:
"""Ensure orchestrator and dependencies are initialized."""
ensure_initialized()
async def _verify_auth_dep(authorization: str | None = Header(default=None)) -> None:
"""Dependency: verify auth for OpenAI bridge routes."""
verify_openai_bridge_auth(authorization)
@router.get("/models", dependencies=[Depends(_verify_auth_dep)])
async def list_models() -> dict[str, Any]:
"""
List available models (OpenAI-compatible).
Returns fusionagi-dvadasa as the single model.
"""
cfg = get_openai_bridge_config()
return {
"object": "list",
"data": [
{
"id": cfg.model_id,
"object": "model",
"created": 1704067200,
"owned_by": "fusionagi",
}
],
}
@router.post(
"/chat/completions",
dependencies=[Depends(_verify_auth_dep)],
response_model=None,
)
async def create_chat_completion(request: Request):
"""
Create chat completion (OpenAI-compatible).
Supports both sync (stream=false) and streaming (stream=true).
"""
_ensure_openai_init()
try:
body = await request.json()
except Exception as e:
raise _openai_error(400, f"Invalid JSON body: {e}", "invalid_request_error")
messages = body.get("messages")
if not messages or not isinstance(messages, list):
raise _openai_error(
400,
"messages is required and must be a non-empty array",
"invalid_request_error",
)
from fusionagi.api.openai_compat.translators import _extract_content
has_content = any(_extract_content(m).strip() for m in messages)
if not has_content:
raise _openai_error(
400,
"messages must contain at least one user or assistant message with content",
"invalid_request_error",
)
prompt = messages_to_prompt(messages)
if not prompt.strip():
raise _openai_error(
400,
"messages must contain at least one user or assistant message with content",
"invalid_request_error",
)
pipeline = get_safety_pipeline()
if pipeline:
pre_result = pipeline.pre_check(prompt)
if not pre_result.allowed:
raise _openai_error(
400,
pre_result.reason or "Input moderation failed",
"invalid_request_error",
)
orch = get_orchestrator()
bus = get_event_bus()
if not orch:
raise _openai_error(503, "Service not initialized", "internal_error")
cfg = get_openai_bridge_config()
request_model = body.get("model") or cfg.model_id
stream = body.get("stream", False) is True
task_id = orch.submit_task(goal=prompt[:200])
parsed = parse_user_input(prompt)
if stream:
return StreamingResponse(
_stream_chat_completion(
orch=orch,
bus=bus,
task_id=task_id,
prompt=prompt,
parsed=parsed,
request_model=request_model,
messages=messages,
pipeline=pipeline,
cfg=cfg,
),
media_type="text/event-stream",
)
# Sync path
final = run_dvadasa(
orchestrator=orch,
task_id=task_id,
user_prompt=prompt,
parsed=parsed,
event_bus=bus,
timeout_per_head=cfg.timeout_per_head,
)
if not final:
raise _openai_error(500, "Dvādaśa failed to produce response", "internal_error")
if pipeline:
post_result = pipeline.post_check(final.final_answer)
if not post_result.passed:
raise _openai_error(
400,
f"Output scan failed: {', '.join(post_result.flags)}",
"invalid_request_error",
)
result = final_response_to_openai(
final=final,
task_id=task_id,
request_model=request_model,
messages=messages,
)
return result
async def _stream_chat_completion(
orch: Any,
bus: Any,
task_id: str,
prompt: str,
parsed: Any,
request_model: str,
messages: list[dict[str, Any]],
pipeline: Any,
cfg: Any,
):
"""
Async generator that runs Dvādaśa and streams the final_answer as SSE chunks.
"""
loop = asyncio.get_event_loop()
executor = ThreadPoolExecutor(max_workers=1)
def run() -> Any:
return run_dvadasa(
orchestrator=orch,
task_id=task_id,
user_prompt=prompt,
parsed=parsed,
event_bus=bus,
timeout_per_head=cfg.timeout_per_head,
)
try:
final = await loop.run_in_executor(executor, run)
except Exception as e:
yield f"data: {json.dumps({'error': {'message': str(e), 'type': 'internal_error'}})}\n\n"
return
if not final:
yield f"data: {json.dumps({'error': {'message': 'Dvādaśa failed', 'type': 'internal_error'}})}\n\n"
return
if pipeline:
post_result = pipeline.post_check(final.final_answer)
if not post_result.passed:
yield f"data: {json.dumps({'error': {'message': 'Output scan failed', 'type': 'invalid_request_error'}})}\n\n"
return
chat_id = f"chatcmpl-{task_id[:24]}" if len(task_id) >= 24 else f"chatcmpl-{task_id}"
# Stream final_answer in chunks
text = final.final_answer
for i in range(0, len(text), _STREAM_CHUNK_SIZE):
chunk = text[i : i + _STREAM_CHUNK_SIZE]
chunk_json = {
"id": chat_id,
"object": "chat.completion.chunk",
"created": 0,
"model": request_model,
"choices": [
{
"index": 0,
"delta": {"content": chunk},
"finish_reason": None,
}
],
}
yield f"data: {json.dumps(chunk_json)}\n\n"
# Final chunk with finish_reason
usage = estimate_usage(messages, text)
final_chunk = {
"id": chat_id,
"object": "chat.completion.chunk",
"created": 0,
"model": request_model,
"choices": [
{
"index": 0,
"delta": {},
"finish_reason": "stop",
}
],
"usage": usage,
}
yield f"data: {json.dumps(final_chunk)}\n\n"
yield "data: [DONE]\n\n"

View File

@@ -0,0 +1,147 @@
"""Session and prompt routes."""
import json
import uuid
from typing import Any
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
from fusionagi.api.dependencies import get_orchestrator, get_session_store, get_event_bus, get_safety_pipeline
from fusionagi.api.websocket import handle_stream
from fusionagi.core import run_dvadasa, select_heads_for_complexity, extract_sources_from_head_outputs
from fusionagi.schemas.commands import parse_user_input, UserIntent
router = APIRouter()
def _ensure_init():
from fusionagi.api.dependencies import ensure_initialized
ensure_initialized()
@router.post("")
def create_session(user_id: str | None = None) -> dict[str, Any]:
"""Create a new session."""
_ensure_init()
store = get_session_store()
if not store:
raise HTTPException(status_code=503, detail="Session store not initialized")
session_id = str(uuid.uuid4())
store.create(session_id, user_id)
return {"session_id": session_id, "user_id": user_id}
@router.post("/{session_id}/prompt")
def submit_prompt(session_id: str, body: dict[str, Any]) -> dict[str, Any]:
"""Submit a prompt and receive FinalResponse (sync)."""
_ensure_init()
store = get_session_store()
orch = get_orchestrator()
bus = get_event_bus()
if not store or not orch:
raise HTTPException(status_code=503, detail="Service not initialized")
sess = store.get(session_id)
if not sess:
raise HTTPException(status_code=404, detail="Session not found")
prompt = body.get("prompt", "")
parsed = parse_user_input(prompt)
if not prompt or not parsed.cleaned_prompt.strip():
if parsed.intent in (UserIntent.SHOW_DISSENT, UserIntent.RERUN_RISK, UserIntent.EXPLAIN_REASONING, UserIntent.SOURCES):
hist = sess.get("history", [])
if hist:
prompt = hist[-1].get("prompt", "")
if not prompt:
raise HTTPException(status_code=400, detail="No previous prompt; provide a prompt for this command")
else:
raise HTTPException(status_code=400, detail="prompt is required")
effective_prompt = parsed.cleaned_prompt.strip() or prompt
pipeline = get_safety_pipeline()
if pipeline:
pre_result = pipeline.pre_check(effective_prompt)
if not pre_result.allowed:
raise HTTPException(status_code=400, detail=pre_result.reason or "Input moderation failed")
task_id = orch.submit_task(goal=effective_prompt[:200])
# Dynamic head selection
head_ids = select_heads_for_complexity(effective_prompt)
if parsed.intent.value == "head_strategy" and parsed.head_id:
head_ids = [parsed.head_id]
force_second = parsed.intent == UserIntent.RERUN_RISK
return_heads = parsed.intent == UserIntent.SOURCES
result = run_dvadasa(
orchestrator=orch,
task_id=task_id,
user_prompt=effective_prompt,
parsed=parsed,
head_ids=head_ids if parsed.intent.value != "normal" or body.get("use_all_heads") else None,
event_bus=bus,
force_second_pass=force_second,
return_head_outputs=return_heads,
)
if return_heads and isinstance(result, tuple):
final, head_outputs = result
else:
final = result
head_outputs = []
if not final:
raise HTTPException(status_code=500, detail="Failed to produce response")
if pipeline:
post_result = pipeline.post_check(final.final_answer)
if not post_result.passed:
raise HTTPException(
status_code=400,
detail=f"Output scan failed: {', '.join(post_result.flags)}",
)
entry = {
"prompt": effective_prompt,
"final_answer": final.final_answer,
"confidence_score": final.confidence_score,
"head_contributions": final.head_contributions,
}
store.append_history(session_id, entry)
response: dict[str, Any] = {
"task_id": task_id,
"final_answer": final.final_answer,
"transparency_report": final.transparency_report.model_dump(),
"head_contributions": final.head_contributions,
"confidence_score": final.confidence_score,
}
if parsed.intent == UserIntent.SHOW_DISSENT:
response["response_mode"] = "show_dissent"
response["disputed_claims"] = final.transparency_report.agreement_map.disputed_claims
elif parsed.intent == UserIntent.EXPLAIN_REASONING:
response["response_mode"] = "explain"
elif parsed.intent == UserIntent.SOURCES and head_outputs:
response["sources"] = extract_sources_from_head_outputs(head_outputs)
return response
@router.websocket("/{session_id}/stream")
async def stream_websocket(websocket: WebSocket, session_id: str) -> None:
"""WebSocket for streaming Dvādaśa response. Send {\"prompt\": \"...\"} to start."""
await websocket.accept()
try:
data = await websocket.receive_json()
prompt = data.get("prompt", "")
async def send_evt(evt: dict) -> None:
await websocket.send_json(evt)
await handle_stream(session_id, prompt, send_evt)
except WebSocketDisconnect:
pass
except Exception as e:
try:
await websocket.send_json({"type": "error", "message": str(e)})
except Exception:
pass

View File

@@ -0,0 +1,49 @@
"""TTS synthesis routes for per-head voice output."""
from typing import Any
from fastapi import APIRouter, HTTPException
from fusionagi.api.dependencies import get_session_store
from fusionagi.config.head_voices import get_voice_id_for_head
from fusionagi.schemas.head import HeadId
router = APIRouter()
@router.post("/{session_id}/synthesize")
async def synthesize(
session_id: str,
body: dict[str, Any],
) -> dict[str, Any]:
"""
Synthesize text to audio for a head.
Body: { "text": "...", "head_id": "logic" }
Returns: { "audio_base64": "..." } or { "audio_base64": null } if TTS not configured.
"""
store = get_session_store()
if not store:
raise HTTPException(status_code=503, detail="Service not initialized")
sess = store.get(session_id)
if not sess:
raise HTTPException(status_code=404, detail="Session not found")
text = body.get("text", "")
head_id_str = body.get("head_id", "")
if not text:
raise HTTPException(status_code=400, detail="text is required")
try:
head_id = HeadId(head_id_str)
except ValueError:
head_id = HeadId.LOGIC
voice_id = get_voice_id_for_head(head_id)
audio_base64 = None
# TODO: Wire TTSAdapter (ElevenLabs, Azure, etc.) and synthesize
# if tts_adapter:
# audio_bytes = await tts_adapter.synthesize(text, voice_id=voice_id)
# if audio_bytes:
# import base64
# audio_base64 = base64.b64encode(audio_bytes).decode()
return {"audio_base64": audio_base64, "voice_id": voice_id}