Skip to content

Commit 9292789

Browse files
author
JiaDe
committed
fix: comprehensive fixes — Usage real data, agent-container stderr JSON, JiaDe+Peter seed
Backend: - _get_agent_usage_today(): dynamic date instead of hardcoded 2026-03-20 - chatgptEquivalent: calculated from employee count × /bin/zsh.83/day - New /api/v1/usage/by-model endpoint (aggregates by model from DynamoDB) - agent-container server.py: extract JSON from stderr when OpenClaw Gateway fallback occurs Seed data: - seed_dynamodb.py: added JiaDe Wang (SA), Peter Wu (Executive), pos-exec position - seed_ssm_tenants.py: added emp-jiade, emp-peter mappings - seed_workspaces.py: added JiaDe and Peter workspace profiles - seed_routing_conversations.py: added conversations for sess-005/007/008 Frontend: - Usage page By Model tab: real data from /api/v1/usage/by-model instead of hardcoded % - useApi.ts: added useUsageByModel hook README: updated sample org counts (22 employees, 11 positions), added JiaDe+Peter to demo accounts
1 parent 3abde3c commit 9292789

8 files changed

Lines changed: 147 additions & 44 deletions

File tree

enterprise/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,9 @@ The seed scripts create ACME Corp — a B2B SaaS company with:
394394
| | Count | Details |
395395
|-|-------|---------|
396396
| Departments | 13 | 7 top-level + 6 sub-departments |
397-
| Positions | 10 | SA, SDE, DevOps, QA, AE, PM, FA, HR, CSM, Legal |
398-
| Employees | 20 | Each with workspace files in S3 |
399-
| Agents | 20 | 18 personal (1:1) + 2 shared (Help Desk, Onboarding) |
397+
| Positions | 11 | SA, SDE, DevOps, QA, AE, PM, FA, HR, CSM, Legal, Executive |
398+
| Employees | 22 | Each with workspace files in S3 |
399+
| Agents | 22 | 20 personal (1:1) + 2 shared (Help Desk, Onboarding) |
400400
| Skills | 26 | 6 global + 20 department-scoped, 3 layers |
401401
| Knowledge Docs | 12 | Company policies, architecture standards, runbooks, etc. |
402402
| SOUL Templates | 10 | 1 global + 9 position-specific |
@@ -407,8 +407,10 @@ The seed scripts create ACME Corp — a B2B SaaS company with:
407407
| Employee ID | Name | Role | What They See |
408408
|-------------|------|------|--------------|
409409
| emp-z3 | Zhang San | Admin | Full Admin Console |
410+
| emp-jiade | JiaDe Wang | Admin | Full Admin Console (Discord-connected) |
410411
| emp-lin | Lin Xiaoyu | Manager | Product department only |
411412
| emp-mike | Mike Johnson | Manager | Sales department only |
413+
| emp-peter | Peter Wu | Manager | Engineering (Executive, Discord-connected) |
412414
| emp-w5 | Wang Wu | Employee | Portal: SDE Agent |
413415
| emp-carol | Carol Zhang | Employee | Portal: Finance Agent |
414416
| emp-emma | Emma Chen | Employee | Portal: CSM Agent |

enterprise/admin-console/server/main.py

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,22 +1891,42 @@ def dashboard(authorization: str = Header(default="")):
18911891
# Per-agent usage data — reads from DynamoDB (seeded by seed_usage.py)
18921892

18931893
def _get_agent_usage_today() -> dict:
1894-
"""Aggregate today's usage per agent from DynamoDB USAGE# records."""
1894+
"""Aggregate today's usage per agent from DynamoDB USAGE# records.
1895+
Reads today's date dynamically. Falls back to seed date if no data found."""
18951896
from datetime import date as _date
1896-
today = "2026-03-20" # In production: _date.today().isoformat()
1897+
today = _date.today().isoformat()
18971898
all_usage = db.get_usage_by_date(today)
1899+
# Fallback: if no data for today, try seed date (demo mode)
1900+
if not all_usage:
1901+
all_usage = db.get_usage_by_date("2026-03-20")
1902+
# Also merge any other recent dates to capture real Discord usage
1903+
for offset in range(1, 7):
1904+
from datetime import timedelta
1905+
past = (_date.today() - timedelta(days=offset)).isoformat()
1906+
past_usage = db.get_usage_by_date(past)
1907+
for u in past_usage:
1908+
aid = u.get("agentId", "")
1909+
if aid and aid not in {uu.get("agentId") for uu in all_usage}:
1910+
all_usage.append(u)
18981911
result = {}
18991912
for u in all_usage:
19001913
aid = u.get("agentId", "")
19011914
if not aid:
19021915
continue
1903-
result[aid] = {
1904-
"inputTokens": u.get("inputTokens", 0),
1905-
"outputTokens": u.get("outputTokens", 0),
1906-
"requests": u.get("requests", 0),
1907-
"cost": float(u.get("cost", 0)),
1908-
"model": u.get("model", ""),
1909-
}
1916+
if aid in result:
1917+
# Accumulate across dates
1918+
result[aid]["inputTokens"] += u.get("inputTokens", 0)
1919+
result[aid]["outputTokens"] += u.get("outputTokens", 0)
1920+
result[aid]["requests"] += u.get("requests", 0)
1921+
result[aid]["cost"] += float(u.get("cost", 0))
1922+
else:
1923+
result[aid] = {
1924+
"inputTokens": u.get("inputTokens", 0),
1925+
"outputTokens": u.get("outputTokens", 0),
1926+
"requests": u.get("requests", 0),
1927+
"cost": float(u.get("cost", 0)),
1928+
"model": u.get("model", ""),
1929+
}
19101930
return result
19111931

19121932
@app.get("/api/v1/usage/summary")
@@ -1917,13 +1937,15 @@ def usage_summary():
19171937
total_cost = sum(u["cost"] for u in usage_map.values())
19181938
total_requests = sum(u["requests"] for u in usage_map.values())
19191939
employees = db.get_employees()
1940+
# ChatGPT Team costs $25/user/month = ~$0.83/user/day
1941+
chatgpt_daily = len([e for e in employees if e.get("agentId")]) * 0.83
19201942
return {
19211943
"totalInputTokens": total_input,
19221944
"totalOutputTokens": total_output,
19231945
"totalCost": round(total_cost, 2),
19241946
"totalRequests": total_requests,
19251947
"tenantCount": len([e for e in employees if e.get("agentId")]),
1926-
"chatgptEquivalent": 5.00,
1948+
"chatgptEquivalent": round(chatgpt_daily, 2),
19271949
}
19281950

19291951
@app.get("/api/v1/usage/by-department")
@@ -1968,6 +1990,41 @@ def usage_by_agent():
19681990
})
19691991
return sorted(result, key=lambda x: x["cost"], reverse=True)
19701992

1993+
@app.get("/api/v1/usage/by-model")
1994+
def usage_by_model():
1995+
"""Aggregate usage by model from DynamoDB USAGE# records."""
1996+
from datetime import date as _date, timedelta
1997+
model_usage: dict = {}
1998+
# Scan last 7 days of usage records
1999+
for offset in range(7):
2000+
d = (_date.today() - timedelta(days=offset)).isoformat()
2001+
records = db.get_usage_by_date(d)
2002+
for u in records:
2003+
model = u.get("model", "unknown")
2004+
if model == "unknown" or not model:
2005+
model = "global.amazon.nova-2-lite-v1:0" # default
2006+
if model not in model_usage:
2007+
model_usage[model] = {"model": model, "inputTokens": 0, "outputTokens": 0, "requests": 0, "cost": 0}
2008+
model_usage[model]["inputTokens"] += u.get("inputTokens", 0)
2009+
model_usage[model]["outputTokens"] += u.get("outputTokens", 0)
2010+
model_usage[model]["requests"] += u.get("requests", 0)
2011+
model_usage[model]["cost"] += float(u.get("cost", 0))
2012+
# Fallback to seed date if empty
2013+
if not model_usage:
2014+
records = db.get_usage_by_date("2026-03-20")
2015+
for u in records:
2016+
model = u.get("model", "global.amazon.nova-2-lite-v1:0")
2017+
if model not in model_usage:
2018+
model_usage[model] = {"model": model, "inputTokens": 0, "outputTokens": 0, "requests": 0, "cost": 0}
2019+
model_usage[model]["inputTokens"] += u.get("inputTokens", 0)
2020+
model_usage[model]["outputTokens"] += u.get("outputTokens", 0)
2021+
model_usage[model]["requests"] += u.get("requests", 0)
2022+
model_usage[model]["cost"] += float(u.get("cost", 0))
2023+
result = sorted(model_usage.values(), key=lambda x: x["cost"], reverse=True)
2024+
for r in result:
2025+
r["cost"] = round(r["cost"], 4)
2026+
return result
2027+
19712028
@app.get("/api/v1/usage/agent/{agent_id}")
19722029
def usage_for_agent(agent_id: str):
19732030
"""Get daily usage records for a specific agent."""

enterprise/admin-console/server/seed_dynamodb.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def seed(table_name: str, region: str):
4747
("pos-hr", "HR Specialist", "dept-hr", "HR & Admin", ["jina-reader","web-search"], ["web_search","file"], 3),
4848
("pos-csm", "Customer Success Manager", "dept-cs", "Customer Success", ["jina-reader","web-search","crm-query","slack-bridge"], ["web_search","file","browser"], 4),
4949
("pos-legal", "Legal Counsel", "dept-legal", "Legal & Compliance", ["jina-reader","deep-research"], ["web_search","file"], 2),
50+
("pos-exec", "Executive", "dept-eng", "Engineering", ["jina-reader","deep-research","web_search"], ["web_search","shell","browser","file","file_write","code_execution"], 1),
5051
]
5152
for pid, name, did, dname, skills, tools, mc in positions:
5253
items.append({"PK": ORG, "SK": f"POS#{pid}", "GSI1PK": "TYPE#pos", "GSI1SK": f"POS#{pid}",
@@ -75,6 +76,8 @@ def seed(table_name: str, region: str):
7576
("emp-jenny", "Jenny Liu", "EMP-018", "pos-hr", "HR Specialist", "dept-hr", "HR & Admin", ["feishu","dingtalk"], "agent-hr-jenny", "active"),
7677
("emp-emma", "Emma Chen", "EMP-019", "pos-csm", "Customer Success Manager", "dept-cs", "Customer Success", ["slack","whatsapp"], "agent-csm-emma", "active"),
7778
("emp-rachel", "Rachel Li", "EMP-021", "pos-legal", "Legal Counsel", "dept-legal", "Legal & Compliance", ["slack"], "agent-legal-rachel", "active"),
79+
("emp-jiade", "JiaDe Wang", "EMP-030", "pos-sa", "Solutions Architect", "dept-eng", "Engineering", ["discord","slack"], "agent-sa-jiade", "active"),
80+
("emp-peter", "Peter Wu", "EMP-031", "pos-exec", "Executive", "dept-eng", "Engineering", ["discord"], "agent-exec-peter", "active"),
7881
]
7982
for eid, name, eno, pid, pname, did, dname, chs, aid, ast in employees:
8083
item = {"PK": ORG, "SK": f"EMP#{eid}", "GSI1PK": "TYPE#emp", "GSI1SK": f"EMP#{eid}",
@@ -104,6 +107,8 @@ def seed(table_name: str, region: str):
104107
("agent-hr-jenny", "HR Agent - Jenny", "emp-jenny", "Jenny Liu", "pos-hr", "HR Specialist", "active", 4.1, ["jina-reader","web-search"], ["feishu","dingtalk"]),
105108
("agent-csm-emma", "CSM Agent - Emma", "emp-emma", "Emma Chen", "pos-csm", "Customer Success Manager", "active", 4.6, ["jina-reader","web-search","crm-query","slack-bridge"], ["slack","whatsapp"]),
106109
("agent-legal-rachel", "Legal Agent - Rachel", "emp-rachel", "Rachel Li", "pos-legal", "Legal Counsel", "active", 4.8, ["jina-reader","deep-research"], ["slack"]),
110+
("agent-sa-jiade", "SA Agent - JiaDe", "emp-jiade", "JiaDe Wang", "pos-sa", "Solutions Architect", "active", None, ["jina-reader","deep-research","arch-diagram-gen","cost-calculator"], ["discord","slack"]),
111+
("agent-exec-peter", "Executive Agent - Peter", "emp-peter", "Peter Wu", "pos-exec", "Executive", "active", None, ["jina-reader","deep-research","web_search"], ["discord"]),
107112
("agent-helpdesk", "IT Help Desk Agent", None, "(Shared)", "pos-devops", "DevOps Engineer", "active", 4.0, ["jina-reader","web-search","jira-query"], ["discord","slack"]),
108113
("agent-onboarding", "Onboarding Assistant", None, "(Shared)", "pos-hr", "HR Specialist", "active", 4.3, ["jina-reader","web-search"], ["feishu","slack"]),
109114
]

enterprise/admin-console/server/seed_ssm_tenants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
"emp-jenny": "pos-hr",
2727
"emp-emma": "pos-csm",
2828
"emp-rachel": "pos-legal",
29+
"emp-jiade": "pos-sa",
30+
"emp-peter": "pos-exec",
2931
# Shared agents
3032
"agent-helpdesk": "pos-devops",
3133
"agent-onboarding": "pos-hr",

enterprise/admin-console/server/seed_workspaces.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ def put(s3, bucket, key, content):
3535
"focus": "GDPR compliance review, vendor contract templates", "style": "Cite specific regulations. Always add legal disclaimer.",
3636
"memory": "Updated DPA template for GDPR Article 28. Vendor contract review backlog: 3 pending. SOC 2 audit scheduled May.",
3737
"daily": "Reviewed 2 vendor contracts. Flagged missing data processing addendum in CloudVendor agreement."},
38+
"emp-jiade": {"name": "JiaDe Wang", "role": "Solutions Architect", "dept": "Engineering", "tz": "Asia/Shanghai", "lang": "Chinese preferred, English OK",
39+
"focus": "OpenClaw Enterprise on AgentCore — multi-tenant digital workforce platform", "style": "Technical, concise. Architecture diagrams and cost comparisons.",
40+
"memory": "Leading OpenClaw Enterprise project. Gateway architecture with H2 Proxy + Tenant Router. 22 employees, 22 agents deployed. Discord Bot connected.",
41+
"daily": "Deployed v21 Docker image with stderr JSON fix. Added JiaDe and Peter to DynamoDB. Verified SOUL injection working for both accounts."},
42+
"emp-peter": {"name": "Peter Wu", "role": "Executive", "dept": "Engineering", "tz": "Asia/Hong_Kong", "lang": "Chinese preferred, English OK",
43+
"focus": "Strategic planning, team management, technology evaluation", "style": "High-level, strategic. Focus on business impact and ROI.",
44+
"memory": "Evaluating OpenClaw Enterprise for team adoption. Interested in cost savings vs ChatGPT Team. Concerned about cold start latency.",
45+
"daily": "Tested Discord Bot integration. Verified role-based access control — no finance/HR data access as Executive role."},
3846
}
3947

4048
def seed():

enterprise/admin-console/src/hooks/useApi.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,13 @@ export function useAgentDailyUsage(agentId: string) {
260260
});
261261
}
262262

263+
export function useUsageByModel() {
264+
return useQuery<{ model: string; inputTokens: number; outputTokens: number; requests: number; cost: number }[]>({
265+
queryKey: ['usage-by-model'],
266+
queryFn: () => api.get('/usage/by-model'),
267+
});
268+
}
269+
263270
export function useUsageBudgets() {
264271
return useQuery<{ department: string; budget: number; used: number; projected: number; status: string }[]>({
265272
queryKey: ['usage-budgets'],

enterprise/admin-console/src/pages/Usage.tsx

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Chart from 'react-apexcharts';
33
import type { ApexOptions } from 'apexcharts';
44
import { DollarSign, TrendingUp, TrendingDown, Users, Bot, AlertTriangle, Download, Calendar } from 'lucide-react';
55
import { Card, StatCard, Badge, Button, PageHeader, Table, Tabs } from '../components/ui';
6-
import { useUsageSummary, useUsageByDepartment, useUsageByAgent, useUsageBudgets, useUsageTrend } from '../hooks/useApi';
6+
import { useUsageSummary, useUsageByDepartment, useUsageByAgent, useUsageBudgets, useUsageTrend, useUsageByModel } from '../hooks/useApi';
77

88
const costTrendOpts: ApexOptions = {
99
chart: { type: 'area', toolbar: { show: false }, background: 'transparent' },
@@ -22,6 +22,7 @@ export default function Usage() {
2222
const { data: summary } = useUsageSummary();
2323
const { data: byDept = [] } = useUsageByDepartment();
2424
const { data: byAgent = [] } = useUsageByAgent();
25+
const { data: byModel = [] } = useUsageByModel();
2526
const { data: budgets = [] } = useUsageBudgets();
2627
const { data: trend = [] } = useUsageTrend();
2728
const [activeTab, setActiveTab] = useState('department');
@@ -170,48 +171,61 @@ export default function Usage() {
170171
<Chart
171172
options={{
172173
chart: { type: 'donut', background: 'transparent' },
173-
colors: ['#22c55e', '#6366f1', '#f59e0b'],
174-
labels: ['Nova 2 Lite', 'Claude Sonnet 4.5', 'Nova Pro'],
174+
colors: ['#22c55e', '#6366f1', '#f59e0b', '#06b6d4', '#ef4444'],
175+
labels: byModel.map(m => m.model.split('/').pop()?.split(':')[0] || m.model),
175176
legend: { position: 'bottom', labels: { colors: '#94a3b8' } },
176-
plotOptions: { pie: { donut: { size: '65%', labels: { show: true, total: { show: true, label: 'Total Tokens', color: '#94a3b8', formatter: () => `${((s.totalInputTokens + s.totalOutputTokens) / 1000).toFixed(0)}k` } } } } },
177+
plotOptions: { pie: { donut: { size: '65%', labels: { show: true, total: { show: true, label: 'Total Tokens', color: '#94a3b8', formatter: () => `${(byModel.reduce((s, m) => s + m.inputTokens + m.outputTokens, 0) / 1000).toFixed(0)}k` } } } } },
177178
dataLabels: { enabled: false },
178179
tooltip: { theme: 'dark' },
179180
}}
180-
series={[
181-
Math.round((s.totalInputTokens + s.totalOutputTokens) * 0.72),
182-
Math.round((s.totalInputTokens + s.totalOutputTokens) * 0.18),
183-
Math.round((s.totalInputTokens + s.totalOutputTokens) * 0.10),
184-
]}
181+
series={byModel.map(m => m.inputTokens + m.outputTokens)}
185182
type="donut" height={300}
186183
/>
187184
</div>
188185
<div>
189186
<h3 className="text-sm font-semibold text-text-primary mb-4">Cost by Model</h3>
190187
<div className="space-y-4">
191-
{[
192-
{ model: 'Nova 2 Lite', id: 'global.amazon.nova-2-lite-v1:0', requests: Math.round(s.totalRequests * 0.72), cost: s.totalCost * 0.45, inputRate: 0.30, outputRate: 2.50, color: '#22c55e', positions: 'Default (all positions)' },
193-
{ model: 'Claude Sonnet 4.5', id: 'global.anthropic.claude-sonnet-4-5', requests: Math.round(s.totalRequests * 0.18), cost: s.totalCost * 0.42, inputRate: 3.00, outputRate: 15.00, color: '#6366f1', positions: 'SA, SDE (override)' },
194-
{ model: 'Nova Pro', id: 'us.amazon.nova-pro-v1:0', requests: Math.round(s.totalRequests * 0.10), cost: s.totalCost * 0.13, inputRate: 0.80, outputRate: 3.20, color: '#f59e0b', positions: 'Finance, Legal (override)' },
195-
].map(m => (
196-
<div key={m.model} className="rounded-lg bg-dark-bg p-4">
197-
<div className="flex items-center justify-between mb-2">
198-
<div className="flex items-center gap-2">
199-
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: m.color }} />
200-
<span className="text-sm font-medium">{m.model}</span>
188+
{(() => {
189+
const colors = ['#22c55e', '#6366f1', '#f59e0b', '#06b6d4', '#ef4444'];
190+
const modelRates: Record<string, { input: number; output: number }> = {
191+
'nova-2-lite': { input: 0.30, output: 2.50 },
192+
'nova-pro': { input: 0.80, output: 3.20 },
193+
'claude-sonnet': { input: 3.00, output: 15.00 },
194+
'claude-haiku': { input: 0.25, output: 1.25 },
195+
};
196+
const totalModelCost = byModel.reduce((s, m) => s + m.cost, 0);
197+
return byModel.map((m, i) => {
198+
const shortName = m.model.split('/').pop()?.split(':')[0] || m.model;
199+
const rateKey = Object.keys(modelRates).find(k => shortName.toLowerCase().includes(k)) || '';
200+
const rates = modelRates[rateKey] || { input: 0.30, output: 2.50 };
201+
const pct = totalModelCost > 0 ? Math.round(m.cost / totalModelCost * 100) : 0;
202+
return (
203+
<div key={m.model} className="rounded-lg bg-dark-bg p-4">
204+
<div className="flex items-center justify-between mb-2">
205+
<div className="flex items-center gap-2">
206+
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: colors[i % colors.length] }} />
207+
<span className="text-sm font-medium">{shortName}</span>
208+
</div>
209+
<span className="text-sm font-semibold" style={{ color: colors[i % colors.length] }}>${m.cost.toFixed(4)}</span>
210+
</div>
211+
<div className="grid grid-cols-3 gap-3 text-xs text-text-muted">
212+
<div><span className="block text-text-secondary">{m.requests}</span>requests</div>
213+
<div><span className="block text-text-secondary">${rates.input}/${rates.output}</span>per 1M tokens</div>
214+
<div><span className="block text-text-secondary">{pct}%</span>of total cost</div>
215+
</div>
201216
</div>
202-
<span className="text-sm font-semibold" style={{ color: m.color }}>${m.cost.toFixed(2)}</span>
203-
</div>
204-
<div className="grid grid-cols-3 gap-3 text-xs text-text-muted">
205-
<div><span className="block text-text-secondary">{m.requests}</span>requests</div>
206-
<div><span className="block text-text-secondary">${m.inputRate}/${m.outputRate}</span>per 1M tokens</div>
207-
<div><span className="block text-text-secondary">{m.positions}</span>assigned to</div>
208-
</div>
209-
</div>
210-
))}
211-
</div>
212-
<div className="mt-4 rounded-lg bg-success/5 border border-success/20 p-3 text-xs text-success">
213-
💡 Nova 2 Lite handles 72% of requests at 45% of cost. Claude Sonnet 4.5 handles 18% of requests but accounts for 42% of cost due to higher per-token pricing.
217+
);
218+
});
219+
})()}
214220
</div>
221+
{byModel.length === 0 && (
222+
<p className="text-sm text-text-muted text-center py-8">No model usage data available</p>
223+
)}
224+
{byModel.length > 0 && (
225+
<div className="mt-4 rounded-lg bg-success/5 border border-success/20 p-3 text-xs text-success">
226+
💡 Data from DynamoDB usage records. Real token counts from AgentCore invocations.
227+
</div>
228+
)}
215229
</div>
216230
</div>
217231
)}

0 commit comments

Comments
 (0)