Cache Composio tools and load saved settings in the UI

Tools are now cached per-org with a 5-minute TTL to avoid hitting the
Composio API on every agent run. The ComposioPanel loads the currently
saved API key status and pre-checks enabled toolkits from the snapshot,
so users see their existing configuration when opening Settings > Tools.
This commit is contained in:
ZacharyZhang-NY committed 2026-06-30 21:58:48 -04:00
1 parent 3d685e5a4b
commit e83f379143
5 files changed
+59 -7

No files matched your search

@@ -3,6 +3,15 @@ import type { ToolDefinition, ToolCall } from "../provider-tool-turn";
import { ComposioClient, type ComposioToolSchema } from "./composio-client";
const COMPOSIO_TOOL_PREFIX = "composio_";
const CACHE_TTL_MS = 5 * 60 * 1000;
interface CacheEntry {
tools: ToolDefinition[];
fetchedAt: number;
toolkitsKey: string;
}
const toolCache = new Map<string, CacheEntry>();
export function isComposioTool(toolName: string): boolean {
return toolName.startsWith(COMPOSIO_TOOL_PREFIX);
@@ -34,9 +43,16 @@ export async function loadComposioTools(db: DatabaseSync, orgId: string): Promis
if (!apiKey) return [];
const toolkits = getEnabledToolkits(db, orgId);
if (toolkits.length === 0) return [];
const toolkitsKey = toolkits.sort().join(",");
const cached = toolCache.get(orgId);
if (cached && cached.toolkitsKey === toolkitsKey && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
return cached.tools;
}
const client = new ComposioClient(apiKey);
const schemas = await client.getTools(toolkits);
return schemas.map(composioSchemaToToolDefinition);
const tools = schemas.map(composioSchemaToToolDefinition);
toolCache.set(orgId, { tools, fetchedAt: Date.now(), toolkitsKey });
return tools;
}
export async function executeComposioTool(
@@ -162,6 +162,7 @@ export class SnapshotService {
.map((rule) => ({ ...rule, enabled: toBool(rule.enabled) })),
meetings: list(this.db, "meetings", "scheduled_at desc"),
sprints: list(this.db, "sprints", "start_date desc"),
composioSettings: list(this.db, "composio_settings", "org_id asc, key asc"),
};
}
@@ -76,7 +76,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
actions={actions}
/>
)}
{tab === "tools" && <ComposioPanel orgId={orgId} actions={actions} />}
{tab === "tools" && <ComposioPanel orgId={orgId} settings={snapshot.composioSettings} actions={actions} />}
{tab === "skills" && (
<SkillsPanel
orgId={orgId}
@@ -1,6 +1,7 @@
import { useState, type FormEvent } from "react";
import { useMemo, useState, type FormEvent } from "react";
import { Button } from "../../shared/ui";
import type { SnapshotActions } from "../../app/useSnapshot";
import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain";
const AVAILABLE_TOOLKITS = [
{ id: "github", label: "GitHub" },
@@ -25,15 +26,35 @@ const AVAILABLE_TOOLKITS = [
{ id: "airtable", label: "Airtable" },
];
function parseSavedToolkits(settings: ComposioSettingRecord[], orgId: string): Set<string> {
const row = settings.find((s) => s.orgId === orgId && s.key === "toolkits");
if (!row?.value) return new Set();
try {
const parsed = JSON.parse(row.value);
return new Set(Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === "string") : []);
} catch {
return new Set();
}
}
function hasSavedKey(settings: ComposioSettingRecord[], orgId: string): boolean {
return settings.some((s) => s.orgId === orgId && s.key === "api_key" && s.value.length > 0);
}
export function ComposioPanel({
orgId,
settings,
actions,
}: {
orgId: string;
settings: ComposioSettingRecord[];
actions: SnapshotActions;
}) {
const savedToolkits = useMemo(() => parseSavedToolkits(settings, orgId), [settings, orgId]);
const keyConfigured = useMemo(() => hasSavedKey(settings, orgId), [settings, orgId]);
const [apiKey, setApiKey] = useState("");
const [selected, setSelected] = useState<Set<string>>(new Set());
const [selected, setSelected] = useState<Set<string>>(savedToolkits);
const [saved, setSaved] = useState(false);
const toggle = (id: string) => {
@@ -47,12 +68,17 @@ export function ComposioPanel({
const handleSave = async (e: FormEvent) => {
e.preventDefault();
if (!apiKey.trim()) return;
const key = apiKey.trim();
if (!key && !keyConfigured) return;
const row = settings.find((s) => s.orgId === orgId && s.key === "api_key");
const effectiveKey = key || row?.value || "";
if (!effectiveKey) return;
await actions.run(() => window.agentCompany.saveComposioSettings({
orgId,
apiKey: apiKey.trim(),
apiKey: effectiveKey,
toolkits: [...selected],
}));
setApiKey("");
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
@@ -71,6 +97,7 @@ export function ComposioPanel({
className="input"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={keyConfigured ? "••••••••" : undefined}
autoComplete="off"
style={{ width: "100%", maxWidth: 400 }}
/>
@@ -109,7 +136,7 @@ export function ComposioPanel({
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<Button tone="primary" type="submit" disabled={!apiKey.trim()}>
<Button tone="primary" type="submit" disabled={!apiKey.trim() && !keyConfigured}>
Save
</Button>
{saved && <span style={{ fontSize: 13, color: "var(--green)" }}>Saved</span>}
@@ -138,4 +138,12 @@ export interface AppSnapshot {
automationRules: AutomationRuleRecord[];
meetings: MeetingRecord[];
sprints: SprintRecord[];
composioSettings: ComposioSettingRecord[];
}
export interface ComposioSettingRecord {
orgId: string;
key: string;
value: string;
updatedAt: string;
}