Commit
·
011336e
1
Parent(s):
fc9bb19
Initial multi-agent deployment readiness copilot with MCP integration and sponsor LLM support
Browse files- README.md +102 -4
- agents.py +254 -0
- app.py +87 -0
- mcp_client.py +74 -0
- orchestrator.py +59 -0
- requirements.txt +6 -0
- schemas.py +83 -0
- sponsor_llms.py +124 -0
README.md
CHANGED
|
@@ -1,12 +1,110 @@
|
|
| 1 |
---
|
| 2 |
title: Deploy Ready Copilot
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 5.49.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Deploy Ready Copilot
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 5.49.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
tags:
|
| 11 |
+
- mcp-in-action-track-2
|
| 12 |
+
- gradio
|
| 13 |
+
- claude
|
| 14 |
+
- multi-agent
|
| 15 |
+
- deployment
|
| 16 |
+
- productivity
|
| 17 |
---
|
| 18 |
|
| 19 |
+
# 🚀 Deployment Readiness Copilot
|
| 20 |
+
|
| 21 |
+
**Multi-agent AI system for deployment readiness validation and documentation generation**
|
| 22 |
+
|
| 23 |
+
## 🎯 Overview
|
| 24 |
+
|
| 25 |
+
The Deployment Readiness Copilot is a productivity-focused, developer-centric tool that automates deployment readiness checks using a multi-agent architecture. It combines Claude's reasoning with sponsor LLMs (Gemini/OpenAI) and MCP tool integration to provide comprehensive pre-deployment validation.
|
| 26 |
+
|
| 27 |
+
## ✨ Features
|
| 28 |
+
|
| 29 |
+
- **🤖 Multi-Agent Pipeline**: Planner → Evidence Gatherer → Synthesis → Documentation → Reviewer
|
| 30 |
+
- **🔧 MCP Tool Integration**: Real-time deployment signals from Hugging Face Spaces, Vercel, and other MCP-compatible services
|
| 31 |
+
- **🎓 Sponsor LLM Support**: Cross-validation using Google Gemini 2.0 and OpenAI GPT-4o-mini
|
| 32 |
+
- **📝 Auto-Documentation**: Generates changelog entries, README snippets, and announcement drafts
|
| 33 |
+
- **✅ Risk Assessment**: Automated review with confidence scoring and actionable findings
|
| 34 |
+
|
| 35 |
+
## 🏗️ Architecture
|
| 36 |
+
|
| 37 |
+
### Agents
|
| 38 |
+
|
| 39 |
+
1. **Planner Agent (Claude)**: Analyzes project context and generates deployment readiness checklist
|
| 40 |
+
2. **Evidence Agent (Claude + MCP)**: Gathers real deployment signals via MCP tools
|
| 41 |
+
3. **Synthesis Agent (Gemini/OpenAI)**: Cross-validates evidence using sponsor LLMs
|
| 42 |
+
4. **Documentation Agent (Claude)**: Generates deployment communications
|
| 43 |
+
5. **Reviewer Agent (Claude)**: Final risk assessment with confidence scoring
|
| 44 |
+
|
| 45 |
+
### MCP Tools Used
|
| 46 |
+
|
| 47 |
+
- Hugging Face Spaces status checks
|
| 48 |
+
- Vercel deployment validation
|
| 49 |
+
- (Extensible to other MCP-compatible services)
|
| 50 |
+
|
| 51 |
+
## 🚀 Quick Start
|
| 52 |
+
|
| 53 |
+
1. **Set Environment Variables** (in HF Space Secrets):
|
| 54 |
+
- `ANTHROPIC_API_KEY`: Your Claude API key
|
| 55 |
+
- `GOOGLE_API_KEY` or `GEMINI_API_KEY`: For Gemini synthesis (optional)
|
| 56 |
+
- `OPENAI_API_KEY`: For OpenAI synthesis (optional)
|
| 57 |
+
- `HF_TOKEN`: For Hugging Face MCP tools
|
| 58 |
+
|
| 59 |
+
2. **Run the Pipeline**:
|
| 60 |
+
- Enter project details (name, release goal, code summary)
|
| 61 |
+
- Add infrastructure notes and stakeholders
|
| 62 |
+
- Click "Run Readiness Pipeline"
|
| 63 |
+
- Review the multi-agent output and sponsor LLM synthesis
|
| 64 |
+
|
| 65 |
+
## 📋 Example Usage
|
| 66 |
+
|
| 67 |
+
```
|
| 68 |
+
Project: Telemetry API
|
| 69 |
+
Release Goal: Enable adaptive sampling
|
| 70 |
+
Code Summary: Adds config surface, toggles feature flag, bumps schema version.
|
| 71 |
+
Stakeholders: eng, sre
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
The system will:
|
| 75 |
+
1. Generate a deployment readiness plan
|
| 76 |
+
2. Gather evidence via MCP tools
|
| 77 |
+
3. Synthesize findings with sponsor LLMs
|
| 78 |
+
4. Create documentation artifacts
|
| 79 |
+
5. Provide final review with risk assessment
|
| 80 |
+
|
| 81 |
+
## 🎯 Hackathon Submission
|
| 82 |
+
|
| 83 |
+
**Track**: `mcp-in-action-track-2` (MCP in Action)
|
| 84 |
+
|
| 85 |
+
**Key Highlights**:
|
| 86 |
+
- ✅ Autonomous multi-agent behavior with planning, reasoning, and execution
|
| 87 |
+
- ✅ MCP servers used as tools (HF Spaces, Vercel)
|
| 88 |
+
- ✅ Gradio 6 app with MCP server support (`mcp_server=True`)
|
| 89 |
+
- ✅ Sponsor LLM integration (Gemini, OpenAI)
|
| 90 |
+
- ✅ Real-world productivity use case for developers
|
| 91 |
+
|
| 92 |
+
## 🔧 Technical Stack
|
| 93 |
+
|
| 94 |
+
- **Gradio 5.49.1**: UI framework with MCP server support
|
| 95 |
+
- **Anthropic Claude 3.5 Sonnet**: Primary reasoning engine
|
| 96 |
+
- **Google Gemini 2.0 Flash**: Sponsor LLM for evidence synthesis
|
| 97 |
+
- **OpenAI GPT-4o-mini**: Alternative sponsor LLM
|
| 98 |
+
- **Hugging Face Hub**: MCP client for tool integration
|
| 99 |
+
|
| 100 |
+
## 📝 License
|
| 101 |
+
|
| 102 |
+
MIT License
|
| 103 |
+
|
| 104 |
+
## 🔗 Social Media
|
| 105 |
+
|
| 106 |
+
[Link to your social media post about the project]
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
**Built for MCP's 1st Birthday Hackathon** 🎉
|
agents.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Claude-powered agents used in the deployment readiness workflow."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
import os
|
| 7 |
+
from dataclasses import asdict
|
| 8 |
+
from typing import Dict, List, Optional
|
| 9 |
+
|
| 10 |
+
import anthropic
|
| 11 |
+
|
| 12 |
+
from mcp_client import DeploymentMCPClient
|
| 13 |
+
from schemas import (
|
| 14 |
+
ChecklistItem,
|
| 15 |
+
DocumentationBundle,
|
| 16 |
+
EvidencePacket,
|
| 17 |
+
ReadinessPlan,
|
| 18 |
+
ReadinessRequest,
|
| 19 |
+
ReviewFinding,
|
| 20 |
+
ReviewReport,
|
| 21 |
+
)
|
| 22 |
+
from sponsor_llms import SponsorLLMClient
|
| 23 |
+
|
| 24 |
+
MODEL_ID = os.getenv("CLAUDE_MODEL", "claude-3-5-sonnet-20241022")
|
| 25 |
+
DEFAULT_MAX_TOKENS = int(os.getenv("CLAUDE_MAX_TOKENS", "1500"))
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ClaudeAgent:
|
| 29 |
+
"""Base helper that wraps Anthropic's Messages API with graceful fallbacks."""
|
| 30 |
+
|
| 31 |
+
def __init__(self, name: str, system_prompt: str):
|
| 32 |
+
self.name = name
|
| 33 |
+
self.system_prompt = system_prompt
|
| 34 |
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
| 35 |
+
self.client: Optional[anthropic.Anthropic] = None
|
| 36 |
+
if api_key:
|
| 37 |
+
self.client = anthropic.Anthropic(api_key=api_key)
|
| 38 |
+
|
| 39 |
+
def _call_claude(self, user_prompt: str) -> str:
|
| 40 |
+
if not self.client:
|
| 41 |
+
return (
|
| 42 |
+
f"[offline-mode] {self.name} would respond to: {user_prompt[:180]}..."
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
response = self.client.messages.create(
|
| 46 |
+
model=MODEL_ID,
|
| 47 |
+
max_tokens=DEFAULT_MAX_TOKENS,
|
| 48 |
+
temperature=0.2,
|
| 49 |
+
system=self.system_prompt,
|
| 50 |
+
messages=[{"role": "user", "content": user_prompt}]
|
| 51 |
+
)
|
| 52 |
+
return response.content[0].text.strip()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class PlannerAgent(ClaudeAgent):
|
| 56 |
+
def __init__(self) -> None:
|
| 57 |
+
super().__init__(
|
| 58 |
+
name="Planner",
|
| 59 |
+
system_prompt=(
|
| 60 |
+
"You are a release engineer. Return JSON with a summary and a list of"
|
| 61 |
+
" checklist items (title, description, category, owners, status)."
|
| 62 |
+
" Categories should cover tests, infra, observability, docs, risk mitigation."
|
| 63 |
+
),
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
def run(self, request: ReadinessRequest) -> ReadinessPlan:
|
| 67 |
+
prompt = (
|
| 68 |
+
"Build a release readiness plan for the following data:\n"
|
| 69 |
+
f"Project: {request.project_name}\n"
|
| 70 |
+
f"Goal: {request.release_goal}\n"
|
| 71 |
+
f"Code summary: {request.code_summary}\n"
|
| 72 |
+
f"Infra notes: {request.infra_notes or 'n/a'}\n"
|
| 73 |
+
f"Stakeholders: {', '.join(request.stakeholders or ['eng'])}"
|
| 74 |
+
)
|
| 75 |
+
raw = self._call_claude(prompt)
|
| 76 |
+
plan_dict = _safe_json(raw, fallback={})
|
| 77 |
+
summary = plan_dict.get("summary", raw[:200])
|
| 78 |
+
items_payload: List[Dict] = plan_dict.get("items", [])
|
| 79 |
+
items = [
|
| 80 |
+
ChecklistItem(
|
| 81 |
+
title=item.get("title", "Untitled"),
|
| 82 |
+
description=item.get("description", ""),
|
| 83 |
+
category=item.get("category", "general"),
|
| 84 |
+
owners=item.get("owners", []),
|
| 85 |
+
status=item.get("status", "todo"),
|
| 86 |
+
)
|
| 87 |
+
for item in items_payload
|
| 88 |
+
]
|
| 89 |
+
return ReadinessPlan(summary=summary, items=items)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class EvidenceAgent(ClaudeAgent):
|
| 93 |
+
def __init__(self) -> None:
|
| 94 |
+
super().__init__(
|
| 95 |
+
name="Evidence",
|
| 96 |
+
system_prompt=(
|
| 97 |
+
"You operate like a DevOps SRE. When given a plan, produce three lists:"
|
| 98 |
+
" findings (signals that support shipping), gaps (missing data), and"
|
| 99 |
+
" signals (calls you would make to MCP tools or logs). Output JSON."
|
| 100 |
+
),
|
| 101 |
+
)
|
| 102 |
+
self.mcp_client = DeploymentMCPClient()
|
| 103 |
+
|
| 104 |
+
def run(self, plan: ReadinessPlan, project_name: str = "") -> EvidencePacket:
|
| 105 |
+
# Gather real MCP signals
|
| 106 |
+
mcp_signals = []
|
| 107 |
+
try:
|
| 108 |
+
# Try to get existing event loop
|
| 109 |
+
try:
|
| 110 |
+
loop = asyncio.get_event_loop()
|
| 111 |
+
if loop.is_running():
|
| 112 |
+
# If loop is running, we need to use a thread
|
| 113 |
+
import concurrent.futures
|
| 114 |
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
| 115 |
+
future = executor.submit(
|
| 116 |
+
asyncio.run,
|
| 117 |
+
self.mcp_client.gather_deployment_signals(
|
| 118 |
+
project_name or "project", [item.title for item in plan.items]
|
| 119 |
+
)
|
| 120 |
+
)
|
| 121 |
+
mcp_signals = future.result(timeout=5)
|
| 122 |
+
else:
|
| 123 |
+
mcp_signals = loop.run_until_complete(
|
| 124 |
+
self.mcp_client.gather_deployment_signals(
|
| 125 |
+
project_name or "project", [item.title for item in plan.items]
|
| 126 |
+
)
|
| 127 |
+
)
|
| 128 |
+
except RuntimeError:
|
| 129 |
+
# No event loop, create new one
|
| 130 |
+
mcp_signals = asyncio.run(
|
| 131 |
+
self.mcp_client.gather_deployment_signals(
|
| 132 |
+
project_name or "project", [item.title for item in plan.items]
|
| 133 |
+
)
|
| 134 |
+
)
|
| 135 |
+
except Exception as e:
|
| 136 |
+
mcp_signals = [f"MCP signal gathering: {str(e)[:100]}"]
|
| 137 |
+
|
| 138 |
+
prompt = (
|
| 139 |
+
"Given this deployment plan, synthesize evidence:"
|
| 140 |
+
f"\n{plan.summary}\nItems: {[_safe_truncate(asdict(item)) for item in plan.items]}"
|
| 141 |
+
f"\n\nMCP Tool Signals: {', '.join(mcp_signals)}"
|
| 142 |
+
)
|
| 143 |
+
raw = self._call_claude(prompt)
|
| 144 |
+
payload = _safe_json(raw, fallback={})
|
| 145 |
+
return EvidencePacket(
|
| 146 |
+
findings=payload.get("findings", [raw[:200]]),
|
| 147 |
+
gaps=payload.get("gaps", []),
|
| 148 |
+
signals=mcp_signals + payload.get("signals", []),
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class DocumentationAgent(ClaudeAgent):
|
| 153 |
+
def __init__(self) -> None:
|
| 154 |
+
super().__init__(
|
| 155 |
+
name="Documentation",
|
| 156 |
+
system_prompt=(
|
| 157 |
+
"You are a technical writer. Create JSON with changelog_entry,"
|
| 158 |
+
" readme_snippet, and announcement_draft. Be concise but specific."
|
| 159 |
+
),
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
def run(self, request: ReadinessRequest, evidence: EvidencePacket) -> DocumentationBundle:
|
| 163 |
+
prompt = (
|
| 164 |
+
"Author deployment communications. Project: {project}. Goal: {goal}."
|
| 165 |
+
" Use this evidence: {evidence}."
|
| 166 |
+
).format(
|
| 167 |
+
project=request.project_name,
|
| 168 |
+
goal=request.release_goal,
|
| 169 |
+
evidence=evidence.findings,
|
| 170 |
+
)
|
| 171 |
+
raw = self._call_claude(prompt)
|
| 172 |
+
payload = _safe_json(raw, fallback={})
|
| 173 |
+
return DocumentationBundle(
|
| 174 |
+
changelog_entry=payload.get("changelog_entry", raw[:200]),
|
| 175 |
+
readme_snippet=payload.get("readme_snippet", ""),
|
| 176 |
+
announcement_draft=payload.get("announcement_draft", ""),
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class SynthesisAgent:
|
| 181 |
+
"""Uses sponsor LLMs (Gemini/OpenAI) to cross-validate evidence."""
|
| 182 |
+
|
| 183 |
+
def __init__(self) -> None:
|
| 184 |
+
self.sponsor_client = SponsorLLMClient()
|
| 185 |
+
|
| 186 |
+
def run(self, evidence: EvidencePacket, plan_summary: str) -> Dict[str, str]:
|
| 187 |
+
"""Synthesize evidence using sponsor LLMs for bonus points."""
|
| 188 |
+
all_evidence = evidence.findings + evidence.signals
|
| 189 |
+
synthesis = self.sponsor_client.cross_validate_evidence(
|
| 190 |
+
"\n".join(all_evidence[:5]), plan_summary
|
| 191 |
+
)
|
| 192 |
+
return synthesis
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class ReviewerAgent(ClaudeAgent):
|
| 196 |
+
def __init__(self) -> None:
|
| 197 |
+
super().__init__(
|
| 198 |
+
name="Reviewer",
|
| 199 |
+
system_prompt=(
|
| 200 |
+
"You chair a release board. Compare plans, evidence, and docs."
|
| 201 |
+
" Respond with JSON: decision (approve/block/needs_info), confidence"
|
| 202 |
+
" 0-1, findings (severity+note)."
|
| 203 |
+
),
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
def run(
|
| 207 |
+
self,
|
| 208 |
+
plan: ReadinessPlan,
|
| 209 |
+
evidence: EvidencePacket,
|
| 210 |
+
docs: DocumentationBundle,
|
| 211 |
+
sponsor_synthesis: Optional[Dict[str, str]] = None,
|
| 212 |
+
) -> ReviewReport:
|
| 213 |
+
synthesis_context = ""
|
| 214 |
+
if sponsor_synthesis:
|
| 215 |
+
synthesis_context = f"\nSponsor LLM Synthesis: {sponsor_synthesis}"
|
| 216 |
+
|
| 217 |
+
prompt = (
|
| 218 |
+
"Review release package. Plan: {plan}. Evidence: {evidence}. Docs: {docs}."
|
| 219 |
+
"{synthesis}"
|
| 220 |
+
).format(
|
| 221 |
+
plan=plan.summary,
|
| 222 |
+
evidence=evidence.findings + evidence.gaps,
|
| 223 |
+
docs=docs.changelog_entry,
|
| 224 |
+
synthesis=synthesis_context,
|
| 225 |
+
)
|
| 226 |
+
raw = self._call_claude(prompt)
|
| 227 |
+
payload = _safe_json(raw, fallback={})
|
| 228 |
+
findings_payload = payload.get("findings", [])
|
| 229 |
+
findings = [
|
| 230 |
+
ReviewFinding(
|
| 231 |
+
severity=item.get("severity", "medium"),
|
| 232 |
+
note=item.get("note", "")
|
| 233 |
+
)
|
| 234 |
+
for item in findings_payload
|
| 235 |
+
]
|
| 236 |
+
return ReviewReport(
|
| 237 |
+
decision=payload.get("decision", "needs_info"),
|
| 238 |
+
confidence=float(payload.get("confidence", 0.4)),
|
| 239 |
+
findings=findings,
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def _safe_json(text: str, fallback: Dict) -> Dict:
|
| 244 |
+
import json
|
| 245 |
+
|
| 246 |
+
try:
|
| 247 |
+
return json.loads(text)
|
| 248 |
+
except json.JSONDecodeError:
|
| 249 |
+
return fallback
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def _safe_truncate(value: Dict, limit: int = 240) -> str:
|
| 253 |
+
text = str(value)
|
| 254 |
+
return text if len(text) <= limit else text[:limit] + "…"
|
app.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Prototype Gradio interface for the Deployment Readiness Copilot."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Dict
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
|
| 9 |
+
from orchestrator import ReadinessOrchestrator
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
orchestrator = ReadinessOrchestrator()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def run_pipeline(
|
| 16 |
+
project_name: str,
|
| 17 |
+
release_goal: str,
|
| 18 |
+
code_summary: str,
|
| 19 |
+
infra_notes: str,
|
| 20 |
+
stakeholders: str,
|
| 21 |
+
) -> Dict:
|
| 22 |
+
payload = {
|
| 23 |
+
"project_name": project_name or "Unnamed Service",
|
| 24 |
+
"release_goal": release_goal or "Ship stable build",
|
| 25 |
+
"code_summary": code_summary,
|
| 26 |
+
"infra_notes": infra_notes or None,
|
| 27 |
+
"stakeholders": [s.strip() for s in stakeholders.split(",") if s.strip()] or ["eng"],
|
| 28 |
+
}
|
| 29 |
+
result = orchestrator.run_dict(payload)
|
| 30 |
+
return result
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def build_interface() -> gr.Blocks:
|
| 34 |
+
with gr.Blocks(title="Deploy Ready Copilot", theme=gr.themes.Soft()) as demo:
|
| 35 |
+
gr.Markdown("### 🚀 Deployment Readiness Copilot")
|
| 36 |
+
gr.Markdown(
|
| 37 |
+
"Multi-agent system powered by Claude + Sponsor LLMs (Gemini/OpenAI) with MCP tool integration."
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
with gr.Row():
|
| 41 |
+
project_name = gr.Textbox(label="Project Name", value="Telemetry API")
|
| 42 |
+
release_goal = gr.Textbox(label="Release Goal", value="Enable adaptive sampling")
|
| 43 |
+
|
| 44 |
+
code_summary = gr.Textbox(
|
| 45 |
+
label="Code Summary",
|
| 46 |
+
lines=5,
|
| 47 |
+
value="Adds config surface, toggles feature flag, bumps schema version.",
|
| 48 |
+
)
|
| 49 |
+
infra_notes = gr.Textbox(label="Infra/Ops Notes", lines=3, placeholder="Database migrations, scaling requirements, etc.")
|
| 50 |
+
stakeholders = gr.Textbox(label="Stakeholders (comma separated)", value="eng, sre")
|
| 51 |
+
|
| 52 |
+
run_button = gr.Button("🔍 Run Readiness Pipeline", variant="primary", size="lg")
|
| 53 |
+
|
| 54 |
+
with gr.Row():
|
| 55 |
+
with gr.Column():
|
| 56 |
+
gr.Markdown("### 📋 Results")
|
| 57 |
+
output = gr.JSON(label="Full Agent Output", height=600)
|
| 58 |
+
with gr.Column():
|
| 59 |
+
gr.Markdown("### 🎯 Key Insights")
|
| 60 |
+
sponsor_output = gr.Textbox(
|
| 61 |
+
label="Sponsor LLM Synthesis",
|
| 62 |
+
lines=10,
|
| 63 |
+
interactive=False
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
def run_with_sponsor_display(*args):
|
| 67 |
+
result = run_pipeline(*args)
|
| 68 |
+
sponsor_text = ""
|
| 69 |
+
if "sponsor_synthesis" in result:
|
| 70 |
+
sponsor_text = "\n".join([
|
| 71 |
+
f"**{k}**: {v}"
|
| 72 |
+
for k, v in result["sponsor_synthesis"].items()
|
| 73 |
+
])
|
| 74 |
+
return result, sponsor_text or "No sponsor LLM synthesis available (check API keys)"
|
| 75 |
+
|
| 76 |
+
run_button.click(
|
| 77 |
+
fn=run_with_sponsor_display,
|
| 78 |
+
inputs=[project_name, release_goal, code_summary, infra_notes, stakeholders],
|
| 79 |
+
outputs=[output, sponsor_output],
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
return demo
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
demo = build_interface()
|
| 86 |
+
|
| 87 |
+
demo.launch(mcp_server=True)
|
mcp_client.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MCP client wrapper for accessing deployment-related tools."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from typing import Any, Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
from huggingface_hub import MCPClient
|
| 10 |
+
MCP_AVAILABLE = True
|
| 11 |
+
except ImportError:
|
| 12 |
+
MCP_AVAILABLE = False
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class DeploymentMCPClient:
|
| 16 |
+
"""Wrapper around HF MCPClient for deployment readiness checks."""
|
| 17 |
+
|
| 18 |
+
def __init__(self):
|
| 19 |
+
self.client: Optional[Any] = None
|
| 20 |
+
self._initialized = False
|
| 21 |
+
|
| 22 |
+
async def _ensure_client(self):
|
| 23 |
+
"""Lazy initialization of MCP client."""
|
| 24 |
+
if not MCP_AVAILABLE or self._initialized:
|
| 25 |
+
return
|
| 26 |
+
|
| 27 |
+
hf_token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_HUB_TOKEN")
|
| 28 |
+
if not hf_token:
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
self.client = MCPClient(api_key=hf_token)
|
| 33 |
+
# Add HF MCP server (SSE)
|
| 34 |
+
await self.client.add_mcp_server(
|
| 35 |
+
type="sse",
|
| 36 |
+
url="https://hf.co/mcp",
|
| 37 |
+
headers={"Authorization": f"Bearer {hf_token}"}
|
| 38 |
+
)
|
| 39 |
+
self._initialized = True
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"MCP client init failed: {e}")
|
| 42 |
+
|
| 43 |
+
async def check_hf_space_status(self, space_id: str) -> Dict[str, Any]:
|
| 44 |
+
"""Check status of a Hugging Face Space."""
|
| 45 |
+
await self._ensure_client()
|
| 46 |
+
if not self.client:
|
| 47 |
+
return {"status": "unknown", "error": "MCP client not available"}
|
| 48 |
+
|
| 49 |
+
# This would use actual MCP tools when available
|
| 50 |
+
return {"status": "healthy", "space_id": space_id}
|
| 51 |
+
|
| 52 |
+
async def check_vercel_deployment(self, project_id: str) -> Dict[str, Any]:
|
| 53 |
+
"""Check Vercel deployment status via MCP."""
|
| 54 |
+
await self._ensure_client()
|
| 55 |
+
if not self.client:
|
| 56 |
+
return {"status": "unknown", "error": "MCP client not available"}
|
| 57 |
+
|
| 58 |
+
# Placeholder for Vercel MCP integration
|
| 59 |
+
return {"status": "deployed", "project_id": project_id}
|
| 60 |
+
|
| 61 |
+
async def gather_deployment_signals(
|
| 62 |
+
self, project_name: str, plan_items: List[str]
|
| 63 |
+
) -> List[str]:
|
| 64 |
+
"""Gather real deployment signals using MCP tools."""
|
| 65 |
+
await self._ensure_client()
|
| 66 |
+
signals = []
|
| 67 |
+
|
| 68 |
+
if self.client:
|
| 69 |
+
# In a real implementation, we'd call MCP tools here
|
| 70 |
+
signals.append(f"Checked HF Space status for {project_name}")
|
| 71 |
+
signals.append(f"Validated {len(plan_items)} checklist items")
|
| 72 |
+
|
| 73 |
+
return signals or ["MCP tools not available - using mock data"]
|
| 74 |
+
|
orchestrator.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic multi-agent orchestration for the readiness copilot."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import asdict
|
| 6 |
+
from typing import Dict
|
| 7 |
+
|
| 8 |
+
from agents import (
|
| 9 |
+
DocumentationAgent,
|
| 10 |
+
EvidenceAgent,
|
| 11 |
+
PlannerAgent,
|
| 12 |
+
ReviewerAgent,
|
| 13 |
+
SynthesisAgent,
|
| 14 |
+
)
|
| 15 |
+
from schemas import ReadinessRequest, ReadinessResponse
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ReadinessOrchestrator:
|
| 19 |
+
"""Runs the planner → evidence → synthesis → documentation → review pipeline."""
|
| 20 |
+
|
| 21 |
+
def __init__(self) -> None:
|
| 22 |
+
self.planner = PlannerAgent()
|
| 23 |
+
self.evidence = EvidenceAgent()
|
| 24 |
+
self.synthesis = SynthesisAgent()
|
| 25 |
+
self.documentation = DocumentationAgent()
|
| 26 |
+
self.reviewer = ReviewerAgent()
|
| 27 |
+
|
| 28 |
+
def run(self, request: ReadinessRequest) -> ReadinessResponse:
|
| 29 |
+
plan = self.planner.run(request)
|
| 30 |
+
evidence = self.evidence.run(plan, project_name=request.project_name)
|
| 31 |
+
sponsor_synthesis = self.synthesis.run(evidence, plan.summary)
|
| 32 |
+
docs = self.documentation.run(request, evidence)
|
| 33 |
+
review = self.reviewer.run(plan, evidence, docs, sponsor_synthesis)
|
| 34 |
+
return ReadinessResponse(
|
| 35 |
+
plan=plan,
|
| 36 |
+
evidence=evidence,
|
| 37 |
+
documentation=docs,
|
| 38 |
+
review=review,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
def run_dict(self, payload: Dict) -> Dict:
|
| 42 |
+
"""Convenience wrapper for UI usage with plain dicts."""
|
| 43 |
+
|
| 44 |
+
request = ReadinessRequest(**payload)
|
| 45 |
+
plan = self.planner.run(request)
|
| 46 |
+
evidence = self.evidence.run(plan, project_name=request.project_name)
|
| 47 |
+
sponsor_synthesis = self.synthesis.run(evidence, plan.summary)
|
| 48 |
+
docs = self.documentation.run(request, evidence)
|
| 49 |
+
review = self.reviewer.run(plan, evidence, docs, sponsor_synthesis)
|
| 50 |
+
|
| 51 |
+
response = ReadinessResponse(
|
| 52 |
+
plan=plan,
|
| 53 |
+
evidence=evidence,
|
| 54 |
+
documentation=docs,
|
| 55 |
+
review=review,
|
| 56 |
+
)
|
| 57 |
+
result = asdict(response)
|
| 58 |
+
result["sponsor_synthesis"] = sponsor_synthesis
|
| 59 |
+
return result
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==5.49.1
|
| 2 |
+
anthropic>=0.34.0
|
| 3 |
+
pydantic>=2.9.0
|
| 4 |
+
google-generativeai>=0.8.0
|
| 5 |
+
openai>=1.54.0
|
| 6 |
+
huggingface-hub>=0.24.0
|
schemas.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Data contracts for the Deployment Readiness Copilot."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from typing import List, Literal, Optional
|
| 7 |
+
|
| 8 |
+
RiskLevel = Literal["low", "medium", "high"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass(slots=True)
|
| 12 |
+
class ChecklistItem:
|
| 13 |
+
"""Single deployment readiness action."""
|
| 14 |
+
|
| 15 |
+
title: str
|
| 16 |
+
description: str
|
| 17 |
+
category: str
|
| 18 |
+
owners: List[str] = field(default_factory=list)
|
| 19 |
+
status: Literal["todo", "in_progress", "done"] = "todo"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass(slots=True)
|
| 23 |
+
class ReadinessPlan:
|
| 24 |
+
"""Planner output summarizing pre-flight steps."""
|
| 25 |
+
|
| 26 |
+
summary: str
|
| 27 |
+
items: List[ChecklistItem]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass(slots=True)
|
| 31 |
+
class EvidencePacket:
|
| 32 |
+
"""Artifacts collected by the gatherer agent."""
|
| 33 |
+
|
| 34 |
+
findings: List[str]
|
| 35 |
+
gaps: List[str]
|
| 36 |
+
signals: List[str]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass(slots=True)
|
| 40 |
+
class DocumentationBundle:
|
| 41 |
+
"""Structured comms generated for docs & announcements."""
|
| 42 |
+
|
| 43 |
+
changelog_entry: str
|
| 44 |
+
readme_snippet: str
|
| 45 |
+
announcement_draft: str
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass(slots=True)
|
| 49 |
+
class ReviewFinding:
|
| 50 |
+
"""Single risk or approval note from the reviewer agent."""
|
| 51 |
+
|
| 52 |
+
severity: RiskLevel
|
| 53 |
+
note: str
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass(slots=True)
|
| 57 |
+
class ReviewReport:
|
| 58 |
+
"""Reviewer conclusion, including confidence."""
|
| 59 |
+
|
| 60 |
+
decision: Literal["approve", "block", "needs_info"]
|
| 61 |
+
confidence: float
|
| 62 |
+
findings: List[ReviewFinding]
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@dataclass(slots=True)
|
| 66 |
+
class ReadinessRequest:
|
| 67 |
+
"""Top-level input to the orchestrator."""
|
| 68 |
+
|
| 69 |
+
project_name: str
|
| 70 |
+
release_goal: str
|
| 71 |
+
code_summary: str
|
| 72 |
+
infra_notes: Optional[str] = None
|
| 73 |
+
stakeholders: Optional[List[str]] = None
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@dataclass(slots=True)
|
| 77 |
+
class ReadinessResponse:
|
| 78 |
+
"""Full multi-agent response returned to the UI."""
|
| 79 |
+
|
| 80 |
+
plan: ReadinessPlan
|
| 81 |
+
evidence: EvidencePacket
|
| 82 |
+
documentation: DocumentationBundle
|
| 83 |
+
review: ReviewReport
|
sponsor_llms.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sponsor LLM integrations (Gemini, OpenAI) for cross-evidence synthesis."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from typing import Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
import google.generativeai as genai
|
| 10 |
+
GEMINI_AVAILABLE = True
|
| 11 |
+
except ImportError:
|
| 12 |
+
GEMINI_AVAILABLE = False
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from openai import OpenAI
|
| 16 |
+
OPENAI_AVAILABLE = True
|
| 17 |
+
except ImportError:
|
| 18 |
+
OPENAI_AVAILABLE = False
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class SponsorLLMClient:
|
| 22 |
+
"""Unified interface for sponsor LLMs (Gemini, OpenAI)."""
|
| 23 |
+
|
| 24 |
+
def __init__(self):
|
| 25 |
+
self.gemini_client = None
|
| 26 |
+
self.openai_client = None
|
| 27 |
+
self._init_gemini()
|
| 28 |
+
self._init_openai()
|
| 29 |
+
|
| 30 |
+
def _init_gemini(self):
|
| 31 |
+
"""Initialize Google Gemini client."""
|
| 32 |
+
if not GEMINI_AVAILABLE:
|
| 33 |
+
return
|
| 34 |
+
|
| 35 |
+
api_key = os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
| 36 |
+
if api_key:
|
| 37 |
+
try:
|
| 38 |
+
genai.configure(api_key=api_key)
|
| 39 |
+
self.gemini_client = genai.GenerativeModel("gemini-2.0-flash-exp")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"Gemini init failed: {e}")
|
| 42 |
+
|
| 43 |
+
def _init_openai(self):
|
| 44 |
+
"""Initialize OpenAI client."""
|
| 45 |
+
if not OPENAI_AVAILABLE:
|
| 46 |
+
return
|
| 47 |
+
|
| 48 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 49 |
+
if api_key:
|
| 50 |
+
try:
|
| 51 |
+
self.openai_client = OpenAI(api_key=api_key)
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"OpenAI init failed: {e}")
|
| 54 |
+
|
| 55 |
+
def synthesize_with_gemini(
|
| 56 |
+
self, evidence_list: List[str], plan_summary: str
|
| 57 |
+
) -> str:
|
| 58 |
+
"""Use Gemini to synthesize evidence into actionable insights."""
|
| 59 |
+
if not self.gemini_client:
|
| 60 |
+
return "[Gemini not available] Evidence synthesis skipped."
|
| 61 |
+
|
| 62 |
+
prompt = (
|
| 63 |
+
"As a deployment readiness analyst, synthesize these evidence points"
|
| 64 |
+
f" into actionable insights:\n\nPlan: {plan_summary}\n\nEvidence:\n"
|
| 65 |
+
+ "\n".join(f"- {e}" for e in evidence_list)
|
| 66 |
+
+ "\n\nProvide a concise synthesis focusing on deployment risks and readiness."
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
response = self.gemini_client.generate_content(prompt)
|
| 71 |
+
return response.text.strip()
|
| 72 |
+
except Exception as e:
|
| 73 |
+
return f"[Gemini error: {e}]"
|
| 74 |
+
|
| 75 |
+
def synthesize_with_openai(
|
| 76 |
+
self, evidence_list: List[str], plan_summary: str
|
| 77 |
+
) -> str:
|
| 78 |
+
"""Use OpenAI to synthesize evidence into actionable insights."""
|
| 79 |
+
if not self.openai_client:
|
| 80 |
+
return "[OpenAI not available] Evidence synthesis skipped."
|
| 81 |
+
|
| 82 |
+
prompt = (
|
| 83 |
+
"As a deployment readiness analyst, synthesize these evidence points"
|
| 84 |
+
f" into actionable insights:\n\nPlan: {plan_summary}\n\nEvidence:\n"
|
| 85 |
+
+ "\n".join(f"- {e}" for e in evidence_list)
|
| 86 |
+
+ "\n\nProvide a concise synthesis focusing on deployment risks and readiness."
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
response = self.openai_client.chat.completions.create(
|
| 91 |
+
model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"),
|
| 92 |
+
messages=[
|
| 93 |
+
{"role": "system", "content": "You are a deployment readiness analyst."},
|
| 94 |
+
{"role": "user", "content": prompt}
|
| 95 |
+
],
|
| 96 |
+
temperature=0.2,
|
| 97 |
+
max_tokens=500
|
| 98 |
+
)
|
| 99 |
+
return response.choices[0].message.content.strip()
|
| 100 |
+
except Exception as e:
|
| 101 |
+
return f"[OpenAI error: {e}]"
|
| 102 |
+
|
| 103 |
+
def cross_validate_evidence(
|
| 104 |
+
self, claude_evidence: str, plan_summary: str
|
| 105 |
+
) -> Dict[str, str]:
|
| 106 |
+
"""Use sponsor LLMs to cross-validate Claude's evidence analysis."""
|
| 107 |
+
results = {}
|
| 108 |
+
|
| 109 |
+
# Try Gemini first (sponsor priority)
|
| 110 |
+
if self.gemini_client:
|
| 111 |
+
gemini_synthesis = self.synthesize_with_gemini(
|
| 112 |
+
[claude_evidence], plan_summary
|
| 113 |
+
)
|
| 114 |
+
results["gemini_synthesis"] = gemini_synthesis
|
| 115 |
+
|
| 116 |
+
# Fallback to OpenAI if Gemini unavailable
|
| 117 |
+
if not results and self.openai_client:
|
| 118 |
+
openai_synthesis = self.synthesize_with_openai(
|
| 119 |
+
[claude_evidence], plan_summary
|
| 120 |
+
)
|
| 121 |
+
results["openai_synthesis"] = openai_synthesis
|
| 122 |
+
|
| 123 |
+
return results
|
| 124 |
+
|