A demonstration application showcasing how to build multi-agent systems using Angular and Genkit, implementing the tool calling agentic pattern. This project serves as a hands-on codelab for understanding how specialized AI agents can work together to solve complex user requests.
This application demonstrates a multi-agent system architecture where a central orchestrator (concierge agent) coordinates multiple specialized agents to handle different types of user requests. It implements the tool calling agentic pattern, one of the key patterns described in the Genkit Agentic Patterns documentation.
Tool calling is an agentic pattern that extends the capabilities of Large Language Models (LLMs) by allowing them to invoke external functions or APIs. Instead of relying solely on the model's training data, the LLM can:
- Analyze the user's request
- Decide which tool(s) to use
- Execute the appropriate tool(s) with the right parameters
- Synthesize the results into a coherent response
This pattern transforms a static LLM into a dynamic agent that can access real-time information, perform calculations, query databases, or delegate to specialized sub-agents.
This app implements a hierarchical multi-agent system with:
- 1 Orchestrator Agent (Concierge): Routes requests and coordinates responses
- 4 Specialized Agents: Each expert in a specific domain
- π Day Trip Agent: Plans full-day itineraries with real-time information
- π½οΈ Foodie Agent: Recommends restaurants and culinary experiences
- π Weekend Guide Agent: Finds events, concerts, and festivals
- πΊοΈ Transport Agent: Provides navigation and route guidance
User Query β Angular Frontend β Firebase Function β Concierge Agent
β
[Tool Calling Decision]
β
ββββββββββββββββββββββββ΄βββββββββββββββββββββββ
β β β
Day Trip Agent Foodie Agent Weekend Guide Agent
β β β
[Google Search] [Google Search] [Google Search]
β β β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββ
β
Synthesized Response β User
Each specialized agent is defined as a tool that the concierge can call. Here's how the Day Trip Agent is implemented:
// functions/src/index.ts
export const _dayTripAgentToolLogic = ai.defineTool(
{
name: 'dayTripAgentTool',
description: 'Assists with planning day trips',
inputSchema: z.object({
input: z.string(),
history: z.array(conversationMessageSchema).optional(),
}),
outputSchema: z.string(),
},
async ({ input, history }) => {
const response = await ai.generate({
system: DAY_TRIP_AGENT_PROMPT,
messages: [
...toGenkitMessages(history ?? []),
{ role: 'user', content: [{ text: input }] },
],
config: {
googleSearchRetrieval: {}, // Enables real-time web search
},
});
if (!response.text) {
throw new Error('No output from AI');
}
return response.text;
}
);What's happening here:
ai.defineTool()creates a callable function that the LLM can invoke- The
descriptionhelps the LLM understand when to use this tool inputSchemaandoutputSchemadefine the data contract using ZodgoogleSearchRetrievalenables the agent to fetch real-time information from the web- The agent combines its specialized prompt with the user's query
The concierge agent acts as the orchestrator, deciding which specialized agent(s) to invoke:
// functions/src/index.ts
export const _conciergeAgentLogic = ai.defineFlow(
{
name: 'conciergeAgentFlow',
inputSchema: z.object({
input: z.string(),
history: z.array(conversationMessageSchema).optional(),
}),
outputSchema: z.string(),
},
async ({ input, history }) => {
const response = await ai.generate({
system: CONCIERGE_AGENT_PROMPT,
messages: [
...toGenkitMessages(history ?? []),
{ role: 'user', content: [{ text: input }] },
],
tools: [
_dayTripAgentToolLogic,
_foodieAgentToolLogic,
_weekendGuideAgentToolLogic,
_findAndNavigateAgentToolLogic,
],
});
const result = response.text || response.output;
if (!result) {
throw new Error('No output from AI');
}
return result;
}
);What's happening here:
ai.defineFlow()creates a workflow that can use multiple tools- The
toolsarray provides all available specialized agents - The LLM automatically decides which tool(s) to call based on the user's query
- Genkit handles the tool invocation, parameter passing, and response aggregation
- The concierge synthesizes the final response for the user
Each agent has a specialized system prompt that defines its expertise and behavior:
// functions/src/system-prompt.ts
export const CONCIERGE_AGENT_PROMPT = `
You are a helpful concierge AI assistant. Your role is to answer user questions
by intelligently using the tools provided to you.
Guidelines:
1. **Use Available Tools**: When a user asks a question, analyze which tools
are available and use the most appropriate one(s) to gather information.
2. **Be Comprehensive**: Combine information from multiple tools if needed
to provide a complete answer.
3. **Be Conversational**: Present the information in a friendly, helpful
manner as a concierge would.
4. **Clarify When Needed**: If a user's request is unclear, ask clarifying
questions before using tools.
5. **Provide Context**: When presenting results from tools, add helpful
context and recommendations.
`;
export const DAY_TRIP_AGENT_PROMPT = `
You are the "Spontaneous Day Trip" Generator π - a specialized AI assistant
that creates engaging full-day itineraries.
Your Mission:
Transform a simple mood or interest into a complete day-trip adventure with
real-time details, while respecting a budget.
Guidelines:
1. **Budget-Aware**: Pay close attention to budget hints like 'cheap',
'affordable', or 'splurge'.
2. **Full-Day Structure**: Create morning, afternoon, and evening activities.
3. **Real-Time Focus**: Search for current operating hours and special events.
4. **Mood Matching**: Align suggestions with the requested mood.
`;What's happening here:
- System prompts define each agent's personality, expertise, and behavior
- The concierge prompt emphasizes tool usage and coordination
- Specialized agent prompts focus on domain-specific expertise
- Clear guidelines ensure consistent, high-quality responses
The Angular frontend communicates with the multi-agent system through Firebase Functions:
// src/app/services/core/ai/ai.service.ts
@Injectable({
providedIn: 'root',
})
export class AiService {
private readonly functions = inject(Functions);
sendMessage(query: string, history: ConversationMessage[] = []): Observable<{ data: string }> {
const conciergeAgentFlow = httpsCallable<{ input: string; history: ConversationMessage[] }, string>(
this.functions,
'conciergeAgentFlow'
);
return from(conciergeAgentFlow({ input: query, history }));
}
}What's happening here:
- The service uses Angular's
inject()function for dependency injection httpsCallablecreates a typed function that calls the Firebase backend- The entire multi-agent orchestration happens server-side
- The
historyarray carries prior conversation turns so agents have full context - RxJS observables provide reactive, asynchronous communication
Agents can ask follow-up questions and remember the user's answers across turns. The Angular client maintains the full conversation history in memory and sends it with every request, keeping the backend stateless and scalable.
// ChatComponent β snapshot history before each send
const historySnapshot = this.conversationHistory();
this.conversationHistory.update((h) => [...h, { role: 'user', content: query }]);
this.aiService.sendMessage(query, historySnapshot).subscribe({
next: (response) => {
// Append AI reply so the next turn has full context
this.conversationHistory.update((h) => [...h, { role: 'model', content: response.data }]);
},
});π For a deep-dive into the design decisions, data flow, and full implementation details, see documentation/conversation-context.md.
- Node.js (v18 or higher)
- Angular CLI (
npm install -g @angular/cli) - Firebase CLI (
npm install -g firebase-tools) - A Google Cloud project with Gemini API access
- Clone the repository:
git clone <repository-url>
cd concierge- Install dependencies:
npm install
cd functions
npm install
cd ..- Set up Firebase:
firebase login
firebase init- Configure your Gemini API key:
firebase functions:secrets:set GEMINI_API_KEYng serveNavigate to http://localhost:4200/
cd functions
npm run servenpm run devTry these example queries to see different agents in action:
- Day Trip Agent: "Plan a budget-friendly day trip in San Francisco with an artsy vibe"
- Foodie Agent: "Where can I find the best sushi in Tokyo?"
- Weekend Guide Agent: "What events are happening in New York this weekend?"
- Transport Agent: "How do I get from Times Square to Central Park?"
- Multi-Agent: "Find me a great Italian restaurant in Rome and tell me how to get there from the Colosseum"
- Genkit Agentic Patterns Documentation
- Firebase Genkit Documentation
- Angular Documentation
- Firebase Functions Documentation
- Conversation Context Implementation β how multi-turn context is managed in this project
- Angular 21 - Frontend framework
- Genkit - Google's open-source AI orchestration framework
- Firebase Functions - Serverless backend
- Google Gemini - Large Language Model
- TypeScript - Type-safe development
This project is intended for educational purposes as part of a codelab session on building multi-agent systems.