Skip to content

Commit ca45c6b

Browse files
authored
Merge pull request #3687 from verifywise-ai/develop
Merge develop into master (April 7)
2 parents 0d46c8c + 826c82f commit ca45c6b

File tree

209 files changed

+49309
-15652
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

209 files changed

+49309
-15652
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
Review and beautify all Recharts charts in the specified file (or all files using Recharts if no argument given).
2+
3+
## Strategy: Replace raw Recharts with VWCharts components
4+
5+
The project has reusable chart components at `Clients/src/presentation/components/Charts/VWCharts.tsx`. These replace raw Recharts boilerplate with consistent styling.
6+
7+
### Available components
8+
9+
| Component | Replaces | Key props |
10+
|-----------|----------|-----------|
11+
| `VWBarChart` | `<BarChart>` + axes + grid + tooltip | `data, series, categoryKey, layout` |
12+
| `VWAreaChart` | `<AreaChart>` + gradient defs + axes | `data, series, categoryKey` |
13+
| `VWDonutChart` | `<PieChart>` + `<Pie>` + center label | `data, dataKey, colors, centerValue` |
14+
| `VWLineChart` | `<LineChart>` + axes + grid | `data, series, categoryKey` |
15+
| `vwTooltipStyle` | Inline tooltip `contentStyle` objects | Use as `contentStyle={vwTooltipStyle}` |
16+
| `VWGradient` | Raw `<linearGradient>` in `<defs>` | `id, color, opacity` |
17+
18+
### Additional utilities at `Charts/chartEnhancements.tsx` (on feat/ai-gateway)
19+
20+
| Utility | Use when |
21+
|---------|----------|
22+
| `ChartCard` | Chart needs its own card container |
23+
| `AnimatedNumber` | Stat value should count up on load |
24+
| `GradientProgressBar` | Progress/percentage bar needed |
25+
| `Sparkline` | Stat card has trend data array |
26+
| `PROVIDER_COLORS` / `getProviderColor()` | Chart shows LLM provider data |
27+
28+
## What to do
29+
30+
For each file with Recharts usage:
31+
32+
1. **Replace raw chart code with VWCharts components**
33+
- `<ResponsiveContainer><BarChart>...</BarChart></ResponsiveContainer>``<VWBarChart data={...} series={[...]} categoryKey="..." />`
34+
- `<ResponsiveContainer><AreaChart>...</AreaChart></ResponsiveContainer>``<VWAreaChart data={...} series={[...]} categoryKey="..." />`
35+
- `<ResponsiveContainer><PieChart>...</PieChart></ResponsiveContainer>``<VWDonutChart data={...} dataKey="..." colors={[...]} />`
36+
- `<ResponsiveContainer><LineChart>...</LineChart></ResponsiveContainer>``<VWLineChart data={...} series={[...]} categoryKey="..." />`
37+
38+
2. **Replace inline tooltip styles** with `vwTooltipStyle`
39+
40+
3. **Remove redundant imports** — after migration, remove individual Recharts imports (XAxis, YAxis, CartesianGrid, etc.) that are now handled internally
41+
42+
4. **Apply enhancements where appropriate**
43+
- Add `gradientOpacity` to area charts for subtle fills
44+
- Add `centerValue` / `centerLabel` to donut charts
45+
- Use `AnimatedNumber` for stat card values
46+
- Use `getProviderColor()` for provider-specific colors
47+
48+
## Rules
49+
50+
- Import from relative path to `components/Charts/VWCharts` (adjust `../` depth)
51+
- Do NOT change chart data, queries, or business logic — only presentation
52+
- Do NOT force-fit components where custom behavior is needed (e.g., PerformanceChart with dynamic legend + custom tooltip — may only benefit from `vwTooltipStyle`)
53+
- Keep existing chart dimensions and layout
54+
- Use `palette` tokens from `themes/palette` for any new colors — never hardcode hex
55+
- Preserve all existing interactivity (click handlers, tooltips, legends)
56+
- Run TypeScript check after changes: `cd Clients && npx tsc --noEmit`
57+
58+
## Target file
59+
60+
$ARGUMENTS
61+
62+
If no file specified, search for all files importing from "recharts" under `Clients/src/` and beautify each one.

.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"Bash(find /Users/harsh/Data/vw/verifywise/Clients -type f \\\\\\(-name *.yaml -o -name *.yml -o -name docker-compose* \\\\\\))",
1212
"Bash(find /Users/harsh/Data/vw/verifywise -type f \\\\\\(-name *.yaml -o -name *.yml -o -name docker-compose* \\\\\\) -not -path */node_modules/*)",
1313
"Bash(grep -r \"superadmin\\\\|super.admin\\\\|SuperAdmin\" /Users/harsh/Data/vw/verifywise/Servers/src --include=*.ts -i)",
14-
"Bash(grep:*)"
14+
"Bash(grep:*)",
15+
"Bash(git log:*)",
16+
"Bash(echo \"EXIT=$?\")"
1517
]
1618
}
1719
}

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,8 @@ EvaluationModule/data/uploads/*
4141
nul
4242
agent-plan.md
4343
pdf-templates/
44+
45+
# WebReel generated videos and screenshots
46+
videos/
47+
screenshots/
48+
.webreel/

AIGateway/requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ asyncpg>=0.30.0
1414

1515
# LiteLLM SDK (MIT licensed)
1616
# Pinned to a specific version — updated per VerifyWise release
17-
litellm==1.82.2
17+
litellm==1.83.0
1818

1919
# Database migrations (Alembic)
2020
sqlalchemy[asyncio]>=2.0.0
@@ -26,3 +26,6 @@ presidio-analyzer>=2.2.0
2626
presidio-anonymizer>=2.2.0
2727
# spaCy model: python -m spacy download en_core_web_md
2828
spacy>=3.7.0
29+
30+
# MCP Gateway — Model Context Protocol SDK
31+
mcp>=1.0.0

AIGateway/src/app.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
from routers.prompts import router as prompts_router
2222
from routers.risk import router as risk_router
2323
from routers.cache import router as cache_router
24+
from routers.mcp_agent_keys import router as mcp_agent_keys_router
25+
from routers.mcp_servers import router as mcp_servers_router
26+
from routers.mcp_approvals import router as mcp_approvals_router
27+
from routers.mcp_audit import router as mcp_audit_router
28+
from routers.mcp_guardrails import router as mcp_guardrails_router
29+
from routers.mcp_tools import router as mcp_tools_router
30+
from routers.mcp_proxy import router as mcp_proxy_router
2431
from routers.tenant_chat import router as tenant_chat_router
2532

2633
# Disable LiteLLM verbose logging to prevent key leakage
@@ -75,6 +82,14 @@
7582
app.include_router(risk_router, prefix="/internal", tags=["CRUD"])
7683
app.include_router(cache_router, prefix="/internal", tags=["CRUD"])
7784

85+
# MCP Gateway CRUD routes (internal)
86+
app.include_router(mcp_agent_keys_router, prefix="/internal", tags=["MCP CRUD"])
87+
app.include_router(mcp_servers_router, prefix="/internal", tags=["MCP CRUD"])
88+
app.include_router(mcp_approvals_router, prefix="/internal", tags=["MCP CRUD"])
89+
app.include_router(mcp_audit_router, prefix="/internal", tags=["MCP CRUD"])
90+
app.include_router(mcp_guardrails_router, prefix="/internal", tags=["MCP CRUD"])
91+
app.include_router(mcp_tools_router, prefix="/internal", tags=["MCP CRUD"])
92+
7893
# Tenant proxy routes (Express proxy → Gateway, JWT-authenticated via headers)
7994
# Chat, streaming, embeddings, providers, model catalog
8095
app.include_router(tenant_chat_router, prefix="/internal", tags=["Tenant Proxy"])
@@ -83,6 +98,10 @@
8398
# OpenAI-compatible: /v1/chat/completions, /v1/embeddings, /v1/models
8499
app.include_router(proxy_router, tags=["Proxy"])
85100

101+
# MCP Gateway public routes (Agent SDK → Gateway, authenticated via agent key)
102+
# Streamable HTTP: POST /v1/mcp, GET /v1/mcp
103+
app.include_router(mcp_proxy_router, tags=["MCP Proxy"])
104+
86105

87106
@app.get("/health")
88107
async def health():

AIGateway/src/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,13 @@ class Settings(BaseSettings):
1616
encryption_key: str = ""
1717
express_backend_url: str = "http://localhost:3000"
1818

19+
# MCP Gateway
20+
mcp_tool_cache_ttl_seconds: int = 300
21+
mcp_session_ttl_seconds: int = 3600
22+
mcp_circuit_breaker_threshold: int = 5
23+
mcp_circuit_breaker_timeout_seconds: int = 30
24+
mcp_approval_expiry_seconds: int = 900
25+
mcp_audit_retention_days: int = 30
26+
1927

2028
settings = Settings()
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import json
2+
import secrets
3+
import hashlib
4+
from typing import Any, Optional
5+
from sqlalchemy import text
6+
from database.db import get_db
7+
8+
9+
def generate_agent_key() -> dict:
10+
plain_key = "sk-mcp-" + secrets.token_hex(16)
11+
key_hash = hashlib.sha256(plain_key.encode()).hexdigest()
12+
prefix = plain_key[:13] + "..."
13+
return {
14+
"plain_key": plain_key,
15+
"key_hash": key_hash,
16+
"prefix": prefix,
17+
}
18+
19+
20+
async def get_all_agent_keys(org_id: int) -> list[dict]:
21+
async with get_db() as db:
22+
result = await db.execute(
23+
text("""
24+
SELECT
25+
ak.id,
26+
ak.key_prefix,
27+
ak.name,
28+
ak.description,
29+
ak.allowed_tools,
30+
ak.blocked_tools,
31+
ak.allowed_server_ids,
32+
ak.rate_limit_rpm,
33+
ak.metadata,
34+
ak.expires_at,
35+
ak.is_active,
36+
ak.revoked_at,
37+
ak.created_by,
38+
ak.created_at,
39+
ak.updated_at,
40+
u.name AS created_by_name
41+
FROM ai_gateway_mcp_agent_keys ak
42+
LEFT JOIN users u ON u.id = ak.created_by
43+
WHERE ak.organization_id = :org_id
44+
ORDER BY ak.created_at DESC
45+
"""),
46+
{"org_id": org_id},
47+
)
48+
rows = result.mappings().all()
49+
return [dict(row) for row in rows]
50+
return []
51+
52+
53+
async def create_agent_key(org_id: int, data: dict) -> Optional[dict]:
54+
key_data = generate_agent_key()
55+
56+
allowed_server_ids = data.get("allowed_server_ids") or []
57+
metadata_value = data.get("metadata")
58+
metadata_json = json.dumps(metadata_value) if metadata_value is not None else "{}"
59+
expires_at = data.get("expires_at")
60+
created_by = data.get("created_by")
61+
rate_limit_rpm = data.get("rate_limit_rpm")
62+
name = data.get("name")
63+
description = data.get("description")
64+
allowed_tools = data.get("allowed_tools") or []
65+
blocked_tools = data.get("blocked_tools") or []
66+
67+
async with get_db() as db:
68+
result = await db.execute(
69+
text("""
70+
INSERT INTO ai_gateway_mcp_agent_keys (
71+
organization_id,
72+
key_hash,
73+
key_prefix,
74+
name,
75+
description,
76+
allowed_tools,
77+
blocked_tools,
78+
allowed_server_ids,
79+
rate_limit_rpm,
80+
metadata,
81+
expires_at,
82+
created_by
83+
) VALUES (
84+
:org_id,
85+
:key_hash,
86+
:key_prefix,
87+
:name,
88+
:description,
89+
:allowed_tools,
90+
:blocked_tools,
91+
:allowed_server_ids,
92+
:rate_limit_rpm,
93+
CAST(:metadata AS jsonb),
94+
:expires_at,
95+
:created_by
96+
)
97+
RETURNING
98+
id,
99+
key_prefix,
100+
name,
101+
description,
102+
allowed_tools,
103+
blocked_tools,
104+
allowed_server_ids,
105+
rate_limit_rpm,
106+
metadata,
107+
expires_at,
108+
is_active,
109+
revoked_at,
110+
created_by,
111+
created_at,
112+
updated_at
113+
"""),
114+
{
115+
"org_id": org_id,
116+
"key_hash": key_data["key_hash"],
117+
"key_prefix": key_data["prefix"],
118+
"name": name,
119+
"description": description,
120+
"allowed_tools": allowed_tools,
121+
"blocked_tools": blocked_tools,
122+
"allowed_server_ids": allowed_server_ids,
123+
"rate_limit_rpm": rate_limit_rpm,
124+
"metadata": metadata_json,
125+
"expires_at": expires_at,
126+
"created_by": created_by,
127+
},
128+
)
129+
await db.commit()
130+
row = result.mappings().first()
131+
if row is None:
132+
return None
133+
record = dict(row)
134+
record["plain_key"] = key_data["plain_key"]
135+
return record
136+
return None
137+
138+
139+
async def update_agent_key(org_id: int, key_id: int, data: dict) -> Optional[dict]:
140+
set_clauses = []
141+
params: dict[str, Any] = {"org_id": org_id, "key_id": key_id}
142+
143+
if "name" in data:
144+
set_clauses.append("name = :name")
145+
params["name"] = data["name"]
146+
147+
if "description" in data:
148+
set_clauses.append("description = :description")
149+
params["description"] = data["description"]
150+
151+
for field in ("allowed_tools", "blocked_tools"):
152+
if field in data:
153+
set_clauses.append(f"{field} = :{field}")
154+
params[field] = data[field] if data[field] else []
155+
156+
if "allowed_server_ids" in data:
157+
set_clauses.append("allowed_server_ids = :allowed_server_ids")
158+
params["allowed_server_ids"] = data["allowed_server_ids"] or []
159+
160+
if "rate_limit_rpm" in data:
161+
set_clauses.append("rate_limit_rpm = :rate_limit_rpm")
162+
params["rate_limit_rpm"] = data["rate_limit_rpm"]
163+
164+
if "metadata" in data:
165+
set_clauses.append("metadata = CAST(:metadata AS jsonb)")
166+
metadata_value = data["metadata"]
167+
params["metadata"] = json.dumps(metadata_value) if metadata_value is not None else "{}"
168+
169+
if "expires_at" in data:
170+
set_clauses.append("expires_at = :expires_at")
171+
params["expires_at"] = data["expires_at"]
172+
173+
if not set_clauses:
174+
return None
175+
176+
set_clauses.append("updated_at = NOW()")
177+
178+
sql = f"""
179+
UPDATE ai_gateway_mcp_agent_keys
180+
SET {", ".join(set_clauses)}
181+
WHERE organization_id = :org_id
182+
AND id = :key_id
183+
RETURNING
184+
id,
185+
key_prefix,
186+
name,
187+
description,
188+
allowed_tools,
189+
blocked_tools,
190+
allowed_server_ids,
191+
rate_limit_rpm,
192+
metadata,
193+
expires_at,
194+
is_active,
195+
revoked_at,
196+
created_by,
197+
created_at,
198+
updated_at
199+
"""
200+
201+
async with get_db() as db:
202+
result = await db.execute(text(sql), params)
203+
await db.commit()
204+
row = result.mappings().first()
205+
if row is None:
206+
return None
207+
return dict(row)
208+
return None
209+
210+
211+
async def revoke_agent_key(org_id: int, key_id: int) -> bool:
212+
async with get_db() as db:
213+
result = await db.execute(
214+
text("""
215+
UPDATE ai_gateway_mcp_agent_keys
216+
SET is_active = false, revoked_at = NOW(), updated_at = NOW()
217+
WHERE organization_id = :org_id
218+
AND id = :key_id
219+
AND is_active = true
220+
RETURNING id
221+
"""),
222+
{"org_id": org_id, "key_id": key_id},
223+
)
224+
await db.commit()
225+
row = result.first()
226+
return row is not None
227+
return False
228+
229+
230+
async def delete_agent_key(org_id: int, key_id: int) -> bool:
231+
async with get_db() as db:
232+
result = await db.execute(
233+
text("""
234+
DELETE FROM ai_gateway_mcp_agent_keys
235+
WHERE organization_id = :org_id
236+
AND id = :key_id
237+
AND is_active = false
238+
RETURNING id
239+
"""),
240+
{"org_id": org_id, "key_id": key_id},
241+
)
242+
await db.commit()
243+
row = result.first()
244+
return row is not None
245+
return False

0 commit comments

Comments
 (0)