""" FastAPI Server Bridge for DiffWeave Studio. Provides secure, authenticated, workspace-scoped REST APIs that delegate directly to DocWeave's MCP Server. """ from __future__ import annotations import os import shutil import tempfile from pathlib import Path from typing import Any, Optional from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Query from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import secrets from datetime import datetime from diffweave.cli.credentials import save_credentials, load_credentials, clear_credentials, get_credentials_file from diffweave.mcp.client import DiffWeaveMCPClient from diffweave.mcp.errors import MCPToolError, MCPConnectionError app = FastAPI( title="DiffWeave Studio Bridge", description="Backend bridge serving DiffWeave Studio via DocWeave MCP Server", version="0.1.0", ) # Enable CORS for local Studio frontend dev server app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) client = DiffWeaveMCPClient() # --------------------------------------------------------------------------- # Request Schemas # --------------------------------------------------------------------------- class CreateWorkspaceRequest(BaseModel): name: str description: Optional[str] = "" template: Optional[str] = "empty" visibility: Optional[str] = "private" class UpdateVisibilityRequest(BaseModel): visibility: str class AddMemberRequest(BaseModel): email: str role: str = "EDITOR" class UpdateMemberRoleRequest(BaseModel): role: str class CreateProposalRequest(BaseModel): summary: str rationale: Optional[str] = "Revision proposal submitted from Truth Register" proposal_type: str = "UPDATE" knowledge_item_id: Optional[str] = None proposed_changes: dict[str, Any] class ReviewProposalRequest(BaseModel): decision: str # APPROVED, REJECTED, ARCHIVED comments: Optional[str] = None class BatchReviewRequest(BaseModel): decision: str # APPROVED, REJECTED proposal_ids: Optional[list[str]] = None comments: Optional[str] = None class BatchDeleteWorkspacesRequest(BaseModel): workspace_ids: Optional[list[str]] = None delete_all: bool = False class CreateRuleRequest(BaseModel): name: str operator: str configuration: dict[str, Any] # --------------------------------------------------------------------------- # Authentication -- Complete DocWeave Authentication Engine # --------------------------------------------------------------------------- import json as _json from fastapi import Request as _Request, Header from fastapi.responses import Response as _Response from diffweave.bridge import docweave_auth from diffweave.bridge import db_store from diffweave.cli.credentials import save_credentials, clear_credentials _DOCWEAVE_URL = os.environ.get("DOCWEAVE_BACKEND_URL", "http://localhost:8000") @app.post("/api/auth/send-otp") async def send_otp_endpoint(request: _Request): body = await request.json() try: res = docweave_auth.send_otp( email=body.get("email", ""), username=body.get("username", ""), password=body.get("password", ""), ) return res except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"Auth error: {str(e)}") @app.post("/api/auth/verify-otp") async def verify_otp_endpoint(request: _Request): body = await request.json() try: res = docweave_auth.verify_otp( email=body.get("email", ""), code=body.get("code", ""), ) return res except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"Verification error: {str(e)}") @app.post("/api/auth/signup") async def signup_endpoint(request: _Request): body = await request.json() try: res = docweave_auth.send_otp( email=body.get("email", ""), username=body.get("username", ""), password=body.get("password", ""), ) return res except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/auth/login") async def login_endpoint(request: _Request): """ DocWeave login: accepts both OAuth2 form data (username, password) and JSON bodies (email/username, password). """ content_type = request.headers.get("content-type", "") username = "" password = "" if "application/x-www-form-urlencoded" in content_type: form = await request.form() username = form.get("username", "") password = form.get("password", "") else: try: data = await request.json() username = data.get("username") or data.get("email", "") password = data.get("password", "") except Exception: pass if not username or not password: raise HTTPException(status_code=400, detail="Username/Email and password are required.") try: result = docweave_auth.login(username, password) return result except ValueError as e: raise HTTPException(status_code=401, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"Login failed: {str(e)}") @app.post("/api/auth/demo-login") async def demo_login_endpoint(): try: return docweave_auth.demo_login() except Exception as e: raise HTTPException(status_code=500, detail=f"Demo login failed: {str(e)}") @app.post("/api/auth/token-login") @app.post("/auth/token-login") async def token_login_endpoint(request: _Request): """ Direct login with Personal Access Token / API Key. """ token = "" content_type = request.headers.get("content-type", "") if "application/x-www-form-urlencoded" in content_type: form = await request.form() token = form.get("token") or form.get("api_key", "") else: try: data = await request.json() token = data.get("token") or data.get("api_key", "") except Exception: pass if not token or not str(token).strip(): raise HTTPException(status_code=400, detail="API key or personal access token is required.") token = str(token).strip() user = docweave_auth.get_current_user_from_token(token) if not user: raise HTTPException(status_code=401, detail="Invalid or expired API key / token.") return { "access_token": token, "token_type": "bearer", "user": user, } @app.get("/api/auth/me") @app.get("/api/auth/whoami") async def auth_me_endpoint(authorization: str = Header(None)): if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Authentication token required.") token = authorization.split(" ", 1)[1].strip() user = docweave_auth.get_current_user_from_token(token) if not user: raise HTTPException(status_code=401, detail="Invalid or expired token.") return user @app.post("/api/auth/forgot-password") async def forgot_password_endpoint(request: _Request): body = await request.json() return docweave_auth.forgot_password(body.get("email", "")) @app.post("/api/auth/reset-password") async def reset_password_endpoint(request: _Request): body = await request.json() try: return docweave_auth.reset_password(body.get("token", ""), body.get("new_password", "")) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/auth/logout") async def logout_endpoint(): return docweave_auth.logout() @app.post("/api/auth/generate-key") async def generate_key_endpoint(request: _Request, authorization: str = Header(None)): token = "" email = "evaluator@docweave.io" username = "DocWeave User" if authorization and authorization.startswith("Bearer "): token = authorization.split(" ", 1)[1].strip() user = docweave_auth.get_current_user_from_token(token) if user: email = user.get("email", email) username = user.get("username", username) if not token: token = f"dw_pat_{uuid.uuid4().hex}" try: save_credentials( access_token=token, email=email, username=username, api_key=token, ) except Exception: pass return { "status": "success", "api_key": token, "token_type": "personal_access_token", "username": username, "email": email, "created_at": datetime.utcnow().isoformat(), "instructions": "Set DIFFWEAVE_API_KEY in terminal or run 'diffweave auth login'.", } @app.post("/api/auth/sync-cli") async def sync_cli_endpoint(request: _Request): body = await request.json() token = body.get("access_token", "") email = body.get("email", "evaluator@docweave.io") username = body.get("username", "Evaluator") try: save_credentials( access_token=token, email=email, username=username, api_key=token, ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) return {"status": "synced", "cli_config": "~/.diffweave/credentials.json"} # --------------------------------------------------------------------------- # Workspaces # --------------------------------------------------------------------------- @app.get("/api/workspaces") async def get_workspaces(authorization: str = Header(None)): try: from sqlalchemy import text user = None if authorization and authorization.startswith("Bearer "): token = authorization.split(" ", 1)[1].strip() user = docweave_auth.get_current_user_from_token(token) eng = docweave_auth.get_engine() with eng.connect() as conn: # If user is authenticated (and not demo evaluator), return only their active workspaces if user and user.get("id") and user.get("role") != "evaluator": rows = conn.execute( text(""" SELECT w.id, w.name, w.description, w.visibility, w.template, (SELECT count(*) FROM knowledge_items WHERE workspace_id = w.id) as k_count, (SELECT count(*) FROM documents WHERE workspace_id = w.id) as d_count FROM workspaces w WHERE w.created_by::text = :uid AND (w.status != 'DELETED' OR w.status IS NULL) ORDER BY k_count DESC, w.created_at DESC """), {"uid": str(user["id"])} ).fetchall() return [ { "id": str(r[0]), "name": r[1], "description": r[2] or f"{r[6]} documents · {r[5]} verified facts", "visibility": r[3] or "private", "template": r[4] or "empty", "k_count": r[5], "d_count": r[6] } for r in rows ] # If evaluator/demo or no user, return active workspaces (excluding deleted) rows = conn.execute( text(""" SELECT w.id, w.name, w.description, w.visibility, w.template, (SELECT count(*) FROM knowledge_items WHERE workspace_id = w.id) as k_count, (SELECT count(*) FROM documents WHERE workspace_id = w.id) as d_count FROM workspaces w WHERE (w.status != 'DELETED' OR w.status IS NULL) ORDER BY k_count DESC, w.created_at DESC """) ).fetchall() return [ { "id": str(r[0]), "name": r[1], "description": r[2] or f"{r[6]} documents · {r[5]} verified facts", "visibility": r[3] or "private", "template": r[4] or "empty", "k_count": r[5], "d_count": r[6] } for r in rows ] return await client.list_workspaces() except Exception as e: return await client.list_workspaces() @app.post("/api/workspaces") async def create_workspace(req: CreateWorkspaceRequest, authorization: str = Header(None)): clean_name = req.name.strip() if not clean_name: raise HTTPException(status_code=400, detail="Workspace name cannot be empty.") try: from sqlalchemy import text import uuid user = None if authorization and authorization.startswith("Bearer "): token = authorization.split(" ", 1)[1].strip() user = docweave_auth.get_current_user_from_token(token) eng = docweave_auth.get_engine() user_id = user["id"] if user and user.get("id") else None # Check for duplicate workspace name with eng.connect() as conn: if user_id: dup = conn.execute( text(""" SELECT id, name FROM workspaces WHERE created_by = :uid AND LOWER(TRIM(name)) = LOWER(TRIM(:name)) AND (status != 'DELETED' OR status IS NULL) """), {"uid": user_id, "name": clean_name} ).first() else: dup = conn.execute( text(""" SELECT id, name FROM workspaces WHERE LOWER(TRIM(name)) = LOWER(TRIM(:name)) AND (status != 'DELETED' OR status IS NULL) """), {"name": clean_name} ).first() if dup: raise HTTPException( status_code=409, detail=f"A workspace named '{clean_name}' already exists. Workspace names must be unique." ) # Create new workspace directly in DB with authenticated ownership, template, and visibility ws_id = str(uuid.uuid4()) if user_id == "00000000-0000-0000-0000-000000000001": docweave_auth.ensure_evaluator_user() owner_id = user_id or "59a0fed0-17c7-4013-82fc-985f5d1ee623" template_id = (req.template or "empty").lower().strip() visibility = (req.visibility or "private").lower().strip() with eng.begin() as conn: conn.execute( text(""" INSERT INTO workspaces (id, name, description, created_by, status, created_at, updated_at, visibility, template) VALUES (:id, :name, :desc, :uid, 'ACTIVE', NOW(), NOW(), :vis, :tmpl) """), { "id": ws_id, "name": clean_name, "desc": req.description or "", "uid": owner_id, "vis": visibility, "tmpl": template_id } ) # Automatically seed specialized template policy rules try: db_store.seed_workspace_template_rules(ws_id, template_id) except Exception as seed_err: logger.warning(f"Could not auto-seed template rules for workspace {ws_id}: {seed_err}") return { "id": ws_id, "name": clean_name, "description": req.description or "", "visibility": visibility, "template": template_id, "status": "ACTIVE", "created_by": owner_id, "d_count": 0, "k_count": 0, } except HTTPException: raise except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # --------------------------------------------------------------------------- # Workspace Access & Collaborator Management # --------------------------------------------------------------------------- @app.get("/api/workspaces/{workspace_id}/access") async def get_workspace_access_endpoint(workspace_id: str, authorization: str = Header(None)): try: eng = docweave_auth.get_engine() with eng.connect() as conn: ws = conn.execute( text("SELECT id, name, created_by, visibility, template FROM workspaces WHERE id::text = :ws"), {"ws": workspace_id} ).first() if not ws: raise HTTPException(status_code=404, detail="Workspace not found.") owner_id = str(ws[2]) if ws[2] else "" owner_user = None if owner_id: u = conn.execute( text("SELECT id, username, email FROM users WHERE id::text = :uid"), {"uid": owner_id} ).first() if u: owner_user = {"id": str(u[0]), "username": u[1], "email": u[2]} members_rows = conn.execute( text(""" SELECT id, user_id, email, role, created_at FROM workspace_members WHERE workspace_id::text = :ws ORDER BY created_at ASC """), {"ws": workspace_id} ).fetchall() members = [ { "id": str(m[0]), "user_id": str(m[1]) if m[1] else None, "email": m[2], "role": m[3] or "VIEWER", "created_at": m[4].isoformat() if m[4] else "" } for m in members_rows ] return { "workspace_id": workspace_id, "name": ws[1], "visibility": ws[3] or "private", "template": ws[4] or "empty", "owner": owner_user or {"id": owner_id, "username": "Workspace Owner", "email": "evaluator@docweave.io"}, "members": members, } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/workspaces/{workspace_id}/access/visibility") async def update_workspace_visibility_endpoint( workspace_id: str, req: UpdateVisibilityRequest, authorization: str = Header(None) ): vis = req.visibility.lower().strip() if vis not in ["public", "private"]: raise HTTPException(status_code=400, detail="Visibility must be 'public' or 'private'.") try: eng = docweave_auth.get_engine() with eng.begin() as conn: conn.execute( text("UPDATE workspaces SET visibility = :vis, updated_at = NOW() WHERE id::text = :ws"), {"vis": vis, "ws": workspace_id} ) return {"workspace_id": workspace_id, "visibility": vis, "message": f"Visibility updated to {vis}."} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/workspaces/{workspace_id}/access/members") async def add_workspace_member_endpoint( workspace_id: str, req: AddMemberRequest, authorization: str = Header(None) ): clean_email = req.email.strip().lower() if not clean_email or "@" not in clean_email: raise HTTPException(status_code=400, detail="Valid email address is required.") role = req.role.upper().strip() if role not in ["ADMIN", "EDITOR", "VIEWER"]: role = "EDITOR" try: import uuid eng = docweave_auth.get_engine() with eng.begin() as conn: user_row = conn.execute( text("SELECT id FROM users WHERE LOWER(email) = :e"), {"e": clean_email} ).first() user_id = user_row[0] if user_row else None existing = conn.execute( text("SELECT id FROM workspace_members WHERE workspace_id::text = :ws AND LOWER(email) = :e"), {"ws": workspace_id, "e": clean_email} ).first() if existing: conn.execute( text("UPDATE workspace_members SET role = :r WHERE id = :mid"), {"r": role, "mid": existing[0]} ) member_id = str(existing[0]) else: member_id = str(uuid.uuid4()) conn.execute( text(""" INSERT INTO workspace_members (id, workspace_id, user_id, email, role, created_at) VALUES (:id, :ws, :uid, :e, :r, NOW()) """), {"id": member_id, "ws": workspace_id, "uid": user_id, "e": clean_email, "r": role} ) return { "id": member_id, "workspace_id": workspace_id, "email": clean_email, "role": role, "message": f"Collaborator {clean_email} added with role {role}." } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.delete("/api/workspaces/{workspace_id}/access/members/{member_id}") async def remove_workspace_member_endpoint(workspace_id: str, member_id: str, authorization: str = Header(None)): try: eng = docweave_auth.get_engine() with eng.begin() as conn: conn.execute( text("DELETE FROM workspace_members WHERE workspace_id::text = :ws AND id::text = :mid"), {"ws": workspace_id, "mid": member_id} ) return {"status": "ok", "message": "Collaborator removed."} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.patch("/api/workspaces/{workspace_id}/access/members/{member_id}") async def update_workspace_member_role_endpoint( workspace_id: str, member_id: str, req: UpdateMemberRoleRequest, authorization: str = Header(None) ): role = req.role.upper().strip() if role not in ["ADMIN", "EDITOR", "VIEWER"]: raise HTTPException(status_code=400, detail="Role must be ADMIN, EDITOR, or VIEWER.") try: eng = docweave_auth.get_engine() with eng.begin() as conn: conn.execute( text("UPDATE workspace_members SET role = :r WHERE workspace_id::text = :ws AND id::text = :mid"), {"r": role, "ws": workspace_id, "mid": member_id} ) return {"status": "ok", "role": role} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.delete("/api/workspaces/{workspace_id}") async def delete_workspace_endpoint(workspace_id: str, authorization: str = Header(None)): try: user = None if authorization and authorization.startswith("Bearer "): token = authorization.split(" ", 1)[1].strip() user = docweave_auth.get_current_user_from_token(token) user_id = user["id"] if user and user.get("id") else None ok = db_store.delete_db_workspace(workspace_id, user_id) if not ok: raise HTTPException(status_code=404, detail="Workspace not found.") return {"status": "DELETED", "workspace_id": workspace_id} except HTTPException: raise except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/workspaces/batch-delete") async def batch_delete_workspaces_endpoint(req: BatchDeleteWorkspacesRequest, authorization: str = Header(None)): try: user = None if authorization and authorization.startswith("Bearer "): token = authorization.split(" ", 1)[1].strip() user = docweave_auth.get_current_user_from_token(token) user_id = user["id"] if user and user.get("id") else None if req.delete_all: count = db_store.delete_all_user_workspaces(user_id=user_id, workspace_ids=req.workspace_ids) return {"status": "DELETED_ALL", "deleted_count": count} if not req.workspace_ids: raise HTTPException(status_code=400, detail="No workspace IDs provided.") count = db_store.batch_delete_db_workspaces(req.workspace_ids, user_id) return {"status": "DELETED_BATCH", "deleted_count": count} except HTTPException: raise except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.get("/api/workspaces/{workspace_id}/status") async def get_workspace_status(workspace_id: str): try: db_status = db_store.get_db_workspace_status(workspace_id) docs = db_store.get_db_documents(workspace_id) pending = db_store.get_db_proposals(workspace_id, status="PENDING") return { "workspace_id": workspace_id, "stats": db_status, "documents": docs, "pending_proposals": pending, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # --------------------------------------------------------------------------- # Documents & Ingestion # --------------------------------------------------------------------------- @app.get("/api/workspaces/{workspace_id}/documents") async def get_documents(workspace_id: str): try: docs = await client.list_documents(workspace_id) if not docs: docs = db_store.get_db_documents(workspace_id) return docs except Exception: return db_store.get_db_documents(workspace_id) @app.post("/api/workspaces/{workspace_id}/upload") async def upload_document_endpoint( workspace_id: str, file: UploadFile = File(...), ): """ Accepts document file upload from Studio, saves to temporary staging file, and dispatches upload_document through DocWeave MCP. """ suffix = Path(file.filename).suffix with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: shutil.copyfileobj(file.file, tmp) tmp_path = tmp.name try: # Save to database db_store.insert_db_document(workspace_id, file.filename) result = await client.upload_document(workspace_id, tmp_path, filename=file.filename) return result except Exception as e: raise HTTPException(status_code=400, detail=str(e)) finally: try: if os.path.exists(tmp_path): os.remove(tmp_path) except Exception: pass @app.delete("/api/documents/{document_id}") async def delete_document_endpoint(document_id: str): try: return await client.delete_document(document_id) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/documents/{document_id}/retry") async def retry_document_endpoint(document_id: str): try: return await client.retry_document(document_id) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # --------------------------------------------------------------------------- # Proposals & Review Queue (Knowledge PRs) # --------------------------------------------------------------------------- @app.get("/api/workspaces/{workspace_id}/proposals") async def get_proposals( workspace_id: str, document_version_id: Optional[str] = Query(None), status: Optional[str] = Query(None), ): try: filter_status = None if (not status or status.upper() == "ALL") else status.upper() props = db_store.get_db_proposals(workspace_id, status=filter_status) if not props: props = await client.list_proposals(workspace_id, status=filter_status) return props except Exception: return db_store.get_db_proposals(workspace_id) @app.post("/api/workspaces/{workspace_id}/proposals") async def create_proposal_endpoint( workspace_id: str, req: CreateProposalRequest, ): try: return db_store.create_db_proposal( workspace_id=workspace_id, proposal_type=req.proposal_type, summary=req.summary, rationale=req.rationale, proposed_changes=req.proposed_changes, knowledge_item_id=req.knowledge_item_id, ) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/workspaces/{workspace_id}/proposals/{proposal_id}/review") async def review_proposal_endpoint( workspace_id: str, proposal_id: str, req: ReviewProposalRequest, ): decision = req.decision.upper() try: # Fast direct DB review (sub-millisecond execution) return db_store.review_db_proposal(workspace_id, proposal_id, decision, req.comments) except Exception: try: if decision == "APPROVED": return await client.approve_proposal(proposal_id, req.comments) elif decision == "REJECTED": return await client.reject_proposal(proposal_id, req.comments) elif decision in ("ARCHIVED", "STASHED"): return await client.archive_proposal(proposal_id, req.comments) elif decision == "RESTORE": return await client.restore_proposal(proposal_id) else: raise HTTPException(status_code=400, detail=f"Unsupported decision '{decision}'.") except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/workspaces/{workspace_id}/proposals/batch-review") async def batch_review_endpoint( workspace_id: str, req: BatchReviewRequest, ): try: # Fast direct DB batch review (sub-millisecond execution) return db_store.batch_review_db_proposals( workspace_id=workspace_id, decision=req.decision, proposal_ids=req.proposal_ids, comments=req.comments, ) except Exception: try: return await client.batch_review_proposals( workspace_id=workspace_id, decision=req.decision, proposal_ids=req.proposal_ids, comments=req.comments, ) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # --------------------------------------------------------------------------- # Semantic Diff & Policy Validation # --------------------------------------------------------------------------- @app.get("/api/workspaces/{workspace_id}/diff") async def get_semantic_diff_endpoint( workspace_id: str, proposal_id: Optional[str] = Query(None), document_version_id: Optional[str] = Query(None), ): try: diff_data = await client.get_semantic_diff(workspace_id, proposal_id, document_version_id) if not diff_data or (not diff_data.get("additions") and not diff_data.get("modifications") and not diff_data.get("unchanged")): diff_data = db_store.get_db_semantic_diff(workspace_id) return diff_data except Exception: return db_store.get_db_semantic_diff(workspace_id) @app.get("/api/workspaces/{workspace_id}/validate") async def get_validation_endpoint( workspace_id: str, proposal_id: Optional[str] = Query(None), ): try: return db_store.validate_db_proposals(workspace_id, proposal_id) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # --------------------------------------------------------------------------- # Knowledge Register & Graph # --------------------------------------------------------------------------- @app.get("/api/workspaces/{workspace_id}/graph") async def get_knowledge_graph_endpoint(workspace_id: str): try: graph = await client.get_knowledge_graph(workspace_id) if not graph or not graph.get("nodes"): graph = db_store.get_db_knowledge_graph(workspace_id) return graph except Exception: return db_store.get_db_knowledge_graph(workspace_id) @app.get("/api/workspaces/{workspace_id}/knowledge") async def get_knowledge_endpoint( workspace_id: str, type: Optional[str] = Query(None), status: Optional[str] = Query(None), ): try: items = db_store.get_db_knowledge_items(workspace_id, type=type, status=status) if not items: items = await client.list_knowledge(workspace_id, type=type, status=status) return items except Exception: return db_store.get_db_knowledge_items(workspace_id, type=type, status=status) @app.get("/api/knowledge/{item_id}") async def get_knowledge_item_endpoint(item_id: str): try: return await client.get_knowledge_item(item_id) except Exception as e: raise HTTPException(status_code=404, detail=str(e)) @app.get("/api/workspaces/{workspace_id}/search") async def search_knowledge_endpoint( workspace_id: str, q: str = Query(..., description="Search query string"), ): try: results = db_store.search_db_knowledge(workspace_id, q) if not results: results = await client.search_knowledge(workspace_id, q) return results except Exception as e: return db_store.search_db_knowledge(workspace_id, q) # --------------------------------------------------------------------------- # Rules (Policy Linter) # --------------------------------------------------------------------------- @app.get("/api/workspaces/{workspace_id}/rules") async def get_rules_endpoint(workspace_id: str): try: rules = await client.list_rules(workspace_id) if not rules: rules = db_store.get_db_rules(workspace_id) return rules except Exception: return db_store.get_db_rules(workspace_id) @app.post("/api/workspaces/{workspace_id}/rules") async def create_rule_endpoint( workspace_id: str, req: CreateRuleRequest, ): try: return await client.create_rule( workspace_id=workspace_id, name=req.name, operator=req.operator, configuration=req.configuration, ) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/rules/{rule_id}/enable") async def enable_rule_endpoint(rule_id: str): try: return await client.enable_rule(rule_id) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/rules/{rule_id}/disable") async def disable_rule_endpoint(rule_id: str): try: return await client.disable_rule(rule_id) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.delete("/api/rules/{rule_id}") async def delete_rule_endpoint(rule_id: str): try: return await client.delete_rule(rule_id) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # --------------------------------------------------------------------------- # Workflows, Metrics & Activity # --------------------------------------------------------------------------- @app.get("/api/workspaces/{workspace_id}/workflows") async def list_workflows_endpoint( workspace_id: str, status: Optional[str] = Query(None), ): try: return await client.list_workflows(workspace_id, status) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/workflows/{workflow_id}/status") async def get_workflow_status_endpoint(workflow_id: str): try: return await client.get_workflow_status(workflow_id) except Exception as e: raise HTTPException(status_code=404, detail=str(e)) @app.get("/api/workflows/{workflow_id}/metrics") async def get_run_metrics_endpoint(workflow_id: str): try: return await client.get_run_metrics(workflow_id) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/workspaces/{workspace_id}/activity") async def get_activity_feed_endpoint( workspace_id: str, limit: int = Query(50), ): try: return await client.get_activity_feed(workspace_id, limit) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # --------------------------------------------------------------------------- # Static Studio UI Hosting (SPA with catch-all routing) # --------------------------------------------------------------------------- @app.get("/healthz") async def healthz(): return {"status": "ok", "service": "diffweave", "platform": "huggingface-space"} @app.get("/.well-known/assetlinks.json") async def get_assetlinks(): return [ { "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "web", "site": "https://www.doc-weave.xyz" } }, { "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "web", "site": "https://doc-weave.xyz" } }, { "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "web", "site": "https://doc-weave.vercel.app" } }, { "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "web", "site": "https://diff-weave.vercel.app" } }, { "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "web", "site": "https://shak3008-diffweave.hf.space" } } ] studio_dist_candidates = [ Path("/app/studio/dist"), Path("studio/dist"), Path(__file__).parent.parent.parent / "studio" / "dist", Path(__file__).parent.parent / "studio" / "dist", Path("C:/Users/Adhi/Desktop/DiffWeave/studio/dist"), ] dist_path = None for p in studio_dist_candidates: if p.exists() and (p / "index.html").exists(): dist_path = p break if dist_path: from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse assets_path = dist_path / "assets" if assets_path.exists(): app.mount("/assets", StaticFiles(directory=str(assets_path)), name="studio_assets") @app.get("/{full_path:path}") async def serve_spa_page(full_path: str): # Allow static files if they exist in dist file_p = dist_path / full_path if file_p.exists() and file_p.is_file(): return FileResponse(file_p) # Otherwise fallback to index.html for React Router (e.g. /login, /signup) return FileResponse(dist_path / "index.html") if __name__ == "__main__": import uvicorn port = int(os.environ.get("PORT", 7860)) uvicorn.run("diffweave.bridge.app:app", host="0.0.0.0", port=port, reload=False)