Skip to content

feat: Add A2A Support to UI #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ This is a Next.js application serving as the UI for the Inference Gateway projec
- [TypeScript SDK](https://github.com/inference-gateway/typescript-sdk)
- [Python SDK](https://github.com/inference-gateway/python-sdk)
- [Documentation](https://docs.inference-gateway.com)
- [A2A ADK](https://github.com/inference-gateway/adk)
- [A2A Agents](https://github.com/inference-gateway/awesome-a2a)
- [A2A Debugger](https://github.com/inference-gateway/a2a-debugger)
- [A2A Google Calendar Agent](https://github.com/inference-gateway/google-calendar-agent)

## Available Tools

Expand Down
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ npm run format:check
npm run lint
npm run typecheck
npm run test:fast
npm run build
36 changes: 36 additions & 0 deletions app/api/v1/a2a/agents/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextResponse } from 'next/server';

export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params;

const inferenceGatewayUrl = process.env.INFERENCE_GATEWAY_URL || 'http://localhost:8080';
const url = `${inferenceGatewayUrl}/v1/a2a/agents/${id}`;

const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

if (!response.ok) {
if (response.status === 404) {
return NextResponse.json({ error: 'Agent not found' }, { status: 404 });
}
console.error(
`Failed to fetch A2A agent details from inference gateway: ${response.statusText}`
);
return NextResponse.json(
{ error: 'Failed to fetch agent details from inference gateway' },
{ status: response.status }
);
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('Error fetching A2A agent details:', error);
return NextResponse.json({ error: 'Failed to fetch agent details' }, { status: 500 });
}
}
29 changes: 29 additions & 0 deletions app/api/v1/a2a/agents/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';

export async function GET() {
try {
const inferenceGatewayUrl = process.env.INFERENCE_GATEWAY_URL || 'http://localhost:8080/v1';
const url = `${inferenceGatewayUrl}/a2a/agents`;

const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

if (!response.ok) {
console.error(`Failed to fetch A2A agents from inference gateway: ${response.statusText}`);
return NextResponse.json(
{ error: 'Failed to fetch A2A agents from inference gateway' },
{ status: response.status }
);
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('Error fetching A2A agents:', error);
return NextResponse.json({ error: 'Failed to fetch A2A agents' }, { status: 500 });
}
}
87 changes: 87 additions & 0 deletions components/a2a-agents-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client';

import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Bot, Loader2 } from 'lucide-react';
import { fetchA2AAgents } from '@/lib/api';
import { A2AAgentsDialog } from './a2a-agents-dialog';
import type { A2AAgent } from '@/types/a2a';

export function A2AAgentsButton() {
const [agents, setAgents] = useState<A2AAgent[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);

const availableAgents = agents.filter(agent => agent.status === 'available');

useEffect(() => {
const loadAgents = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetchA2AAgents();
setAgents(response.data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load A2A agents');
} finally {
setIsLoading(false);
}
};

loadAgents();
}, []);

const handleClick = () => {
setDialogOpen(true);
};

return (
<>
<Button
variant="outline"
size="sm"
onClick={handleClick}
disabled={isLoading}
className="h-8 gap-2"
title={
error
? `Error loading A2A agents: ${error}`
: `${availableAgents.length} A2A agent${availableAgents.length === 1 ? '' : 's'} available`
}
>
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Bot className="h-4 w-4" />}
<span className="text-sm">A2A</span>
{!isLoading && !error && (
<Badge variant={availableAgents.length > 0 ? 'default' : 'secondary'} className="ml-1">
{availableAgents.length}
</Badge>
)}
</Button>

<A2AAgentsDialog
open={dialogOpen}
onOpenChangeAction={setDialogOpen}
agents={agents}
isLoading={isLoading}
error={error}
onRefreshAction={() => {
const loadAgents = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetchA2AAgents();
setAgents(response.data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load A2A agents');
} finally {
setIsLoading(false);
}
};
loadAgents();
}}
/>
</>
);
}
Loading