diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..77088eb5
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,227 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is a full-stack AI agent application combining **CopilotKit**, **A2A (Agent-to-Agent)**, and **A2UI (Agent-to-UI)** frameworks. The application demonstrates a restaurant finder agent that dynamically generates UI components based on LLM responses.
+
+**Stack:**
+- Frontend: Next.js 16 with React 19, Tailwind CSS 4
+- Backend Agent: Python 3.13+ with Google ADK (Agent Development Kit)
+- LLM Provider: OpenAI o1-mini (reasoning model with tool support)
+- Agent Framework: A2A SDK for agent communication
+- UI Protocol: A2UI for declarative UI generation
+- Package Manager: Any (pnpm/npm/yarn/bun) for Node.js, `uv` for Python
+
+## Development Commands
+
+### Starting the Application
+
+```bash
+# Start both UI and agent servers concurrently (recommended)
+pnpm dev
+
+# Start with debug logging
+pnpm dev:debug
+
+# Start only the Next.js UI (port 3000)
+pnpm dev:ui
+
+# Start only the Python agent server (port 10002)
+pnpm dev:agent
+```
+
+### Building and Deployment
+
+```bash
+# Build Next.js for production
+pnpm build
+
+# Start production server
+pnpm start
+
+# Lint code
+pnpm lint
+```
+
+### Python Agent Development
+
+```bash
+# Install/sync Python dependencies manually
+cd agent
+uv sync
+
+# Run agent directly
+uv run .
+
+# Install Python deps from root (handled by postinstall)
+pnpm install:agent
+```
+
+## Architecture
+
+### Two-Server Architecture
+
+The application runs two concurrent servers:
+
+1. **Next.js Frontend** (port 3000)
+ - Located in `app/` directory
+ - Main entry: `app/page.tsx` - renders CopilotChat with A2UI renderer
+ - API route: `app/api/copilotkit/[[...slug]]/route.tsx` - CopilotKit endpoint that connects to A2A agent
+
+2. **Python Agent Server** (port 10002)
+ - Located in `agent/` directory
+ - Entry point: `agent/__main__.py` - A2A server with Starlette/uvicorn
+ - Agent logic: `agent/agent.py` - RestaurantAgent with Google ADK/Gemini
+ - Tools: `agent/tools.py` - get_restaurants tool
+ - UI templates: `agent/prompt_builder.py` - A2UI component schemas and examples
+
+### Communication Flow
+
+```
+User → Next.js UI → CopilotKit Runtime → A2A Client (localhost:10002)
+ → A2A Server → RestaurantAgentExecutor → RestaurantAgent (Google ADK + OpenAI o1-mini)
+ → LLM generates A2UI JSON → Validated against schema → Rendered in UI
+```
+
+### Key Components
+
+**Frontend (`app/`):**
+- `page.tsx`: CopilotKitProvider with A2UIMessageRenderer
+- `api/copilotkit/[[...slug]]/route.tsx`: Creates A2AAgent pointing to localhost:10002
+- `theme.ts`: A2UI theme configuration (colors, fonts, spacing)
+
+**Agent (`agent/`):**
+- `__main__.py`: A2A server setup with CORS, static file serving, agent card definition
+- `agent_executor.py`: RestaurantAgentExecutor - handles UI/text mode switching, processes user actions (book_restaurant, submit_booking)
+- `agent.py`: RestaurantAgent - wraps Google ADK LlmAgent with OpenAI o1-mini, validates A2UI JSON responses, implements retry logic
+- `prompt_builder.py`: Contains A2UI_SCHEMA and RESTAURANT_UI_EXAMPLES (single column list, two column list, booking form, confirmation)
+- `tools.py`: get_restaurants tool that loads from restaurant_data.json
+- `restaurant_data.json`: Mock restaurant data
+
+**A2UI Extension (`a2ui_extension/`):**
+- Custom A2UI extension for the agent (workspace dependency)
+
+### A2UI Architecture
+
+The agent generates declarative UI using A2UI protocol:
+
+1. **UI Templates**: Defined in `agent/prompt_builder.py` as examples (SINGLE_COLUMN_LIST_EXAMPLE, TWO_COLUMN_LIST_EXAMPLE, BOOKING_FORM_EXAMPLE, CONFIRMATION_EXAMPLE)
+2. **Schema Validation**: A2UI_SCHEMA defines valid component types (Text, Image, Button, Card, Row, Column, List, etc.)
+3. **Component Generation**: LLM generates JSON matching schema with three message types:
+ - `beginRendering`: Initialize surface with root component
+ - `surfaceUpdate`: Define components with IDs and hierarchical structure
+ - `dataModelUpdate`: Populate data model with actual content
+4. **Rendering**: Frontend A2UIMessageRenderer converts JSON to React components
+
+### Agent Response Format
+
+Agent responses split into two parts with `---a2ui_JSON---` delimiter:
+1. Text response (conversational)
+2. JSON array of A2UI messages (validated against schema)
+
+## Environment Configuration
+
+Create `agent/.env`:
+```
+# OpenRouter API Key (recommended - access to multiple providers)
+# Get your API key from: https://openrouter.ai/keys
+OPENROUTER_API_KEY=your-openrouter-api-key-here
+
+# OpenAI API Key (optional - for direct OpenAI access)
+# Get your API key from: https://platform.openai.com/api-keys
+OPENAI_API_KEY=your-openai-api-key-here
+
+# Google API Key (optional - for direct Google access)
+# Get your API key from: https://aistudio.google.com/apikey
+GOOGLE_API_KEY=your-google-api-key-here
+
+# Model selection (default: openrouter/google/gemini-2.0-flash-thinking-exp:free)
+#
+# OpenRouter models (use openrouter/ prefix):
+# Free models with TOOL CALLING support (REQUIRED for this app):
+# - openrouter/google/gemini-2.0-flash-thinking-exp:free (RECOMMENDED - Google, reasoning + tools)
+# - openrouter/qwen/qwq-32b-preview:free (Qwen reasoning model with tools)
+# - openrouter/mistralai/mistral-small-3.1:free (Mistral Small, function calling)
+# - openrouter/google/gemini-2.0-flash-exp:free (Google Gemini 2.0, may have rate limits)
+#
+# ⚠️ Models WITHOUT tool support (DO NOT USE):
+# - openrouter/deepseek/deepseek-r1-* (no tool calling support)
+# - openrouter/deepseek/deepseek-chat:free (no tool calling support)
+# Paid models:
+# - openrouter/openai/gpt-4o (OpenAI GPT-4o via OpenRouter)
+# - openrouter/anthropic/claude-3.5-sonnet (Claude 3.5 Sonnet)
+# - openrouter/meta-llama/llama-3.1-70b-instruct (Meta Llama 3.1)
+#
+# Direct provider models (requires respective API keys):
+# - OpenAI: o1-mini, gpt-4o, gpt-4o-mini, gpt-4-turbo
+# - Google: gemini/gemini-2.0-flash-exp, gemini/gemini-2.5-flash-lite
+# - Perplexity (NO tool support): perplexity/llama-3.1-sonar-large-128k-online
+LITELLM_MODEL=openrouter/google/gemini-2.0-flash-exp:free
+
+# Retry and timeout configuration
+LITELLM_NUM_RETRIES=3
+LITELLM_TIMEOUT=60
+```
+
+> **Note:** Get your OpenRouter API key from [https://openrouter.ai/keys](https://openrouter.ai/keys) (recommended) or OpenAI key from [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys)
+
+## File Locations
+
+- Next.js pages/components: `app/`
+- Agent server: `agent/`
+- Python dependencies: `agent/pyproject.toml`
+- Node dependencies: `package.json`
+- Scripts: `scripts/` (setup-agent.sh, run-agent.sh)
+- A2UI extension: `a2ui_extension/src/a2ui/`
+- Static assets (restaurant images): `agent/images/`
+
+## Workspace Structure
+
+UV workspace with two members:
+- `agent/` - Main agent package (a2ui-restaurant-finder)
+- `a2ui_extension/` - Custom A2UI extension
+
+Both managed via root `pyproject.toml` workspace configuration.
+
+## Common Workflows
+
+### Adding New UI Components
+
+1. Design component structure in A2UI format
+2. Add example template to `agent/prompt_builder.py` in RESTAURANT_UI_EXAMPLES
+3. Update agent instructions in `agent/agent.py` to use new template
+4. Optionally use [A2UI Composer](https://a2ui-editor.ag-ui.com) to generate components
+
+### Adding New Agent Tools
+
+1. Define tool function in `agent/tools.py` with Google ADK signature
+2. Add tool to `tools` list in `agent/agent.py` LlmAgent initialization
+3. Update AGENT_INSTRUCTION to document tool usage
+
+### Modifying Theme
+
+Edit `app/theme.ts` to customize:
+- Colors (primary, secondary, accent, background)
+- Fonts
+- Spacing
+- Component styling
+
+## Troubleshooting
+
+**Agent connection errors:**
+- Verify agent server is running on port 10002
+- Check OPENAI_API_KEY is set in `agent/.env`
+- Verify LITELLM_MODEL is set to a valid model (default: o1-mini)
+- Ensure both servers started successfully
+
+**Python import errors:**
+```bash
+cd agent
+uv sync
+```
+
+**Port conflicts:**
+- UI runs on port 3000 (configurable with Next.js)
+- Agent runs on port 10002 (configurable with --port flag in `agent/__main__.py`)
diff --git a/ISTABOT.md b/ISTABOT.md
new file mode 100644
index 00000000..d29b8ac3
--- /dev/null
+++ b/ISTABOT.md
@@ -0,0 +1,33 @@
+a2ui, copilotkit, langgraph/mastra/openserv vs ile istabot.com musterilerimiz icin agent-supported bot insa etmek istiyorum. Oncelikle
+ google un yeni duyurdugu a2ui i ve sonrasinda copilotkit/langraph i anlamama yardimci olur musun
+
+ ```
+ A2UI: Agent-to-User Interface
+ A2UI is an open-source project, complete with a format optimized for representing updateable agent-generated UIs and an initial set of
+ renderers, that allows agents to generate or populate rich user interfaces.
+ https://github.com/google/A2UI
+ ```
+
+ persona
+
+ ```
+ TEMEL KİMLİK VE MİSYON (PERSONA BİLGİSİ)
+ Alan
+ Değer/Tanım
+ Kaynaklar
+ Ajans Rolü
+ Kıdemli Biyoistatistik Uzmanı ve Grafik Motoru Mimarı. Karmaşık istatistiksel analizleri (özellikle klinik araştırmalarda) kodlama
+ gerektirmeden erişilebilir ve hatasız hale getirir.
+ Hedef Kitle
+ Akademisyenler, doktora öğrencileri, araştırmacılar, sağlık profesyonelleri (doktorlar, hemşireler, diş hekimleri). Çıktıların APA
+ formatında ve akademik yayın standartlarında olması zorunludur.
+ Temel Misyon
+ İstatistiksel analiz sürecini karmaşıklıktan arındırarak, hata riskini azaltmak ve kullanıcılara analizlerini güvenle tamamlama yeteneği
+ kazandırmak.
+ Ton ve İletişim
+ Profesyonel, destekleyici ve eğitici. Teknik jargon kullanmaktan kaçınır veya anlaşılır açıklamalarla destekler.
+ ```
+
+ --
+
+ https://www.reddit.com/r/n8n/comments/1oa4kbp/openais_hidden_trick_get_up_to_10m_free_tokens/
\ No newline at end of file
diff --git a/agent/__main__.py b/agent/__main__.py
index a0ec4a65..d269d10c 100644
--- a/agent/__main__.py
+++ b/agent/__main__.py
@@ -42,12 +42,13 @@ class MissingAPIKeyError(Exception):
@click.option("--port", default=10002)
def main(host, port):
try:
- # Check for API key only if Vertex AI is not configured
- if not os.getenv("GOOGLE_GENAI_USE_VERTEXAI") == "TRUE":
- if not os.getenv("GEMINI_API_KEY"):
- raise MissingAPIKeyError(
- "GEMINI_API_KEY environment variable not set and GOOGLE_GENAI_USE_VERTEXAI is not TRUE."
- )
+ # Check for OpenAI API key (required for o1-mini model)
+ if not os.getenv("OPENAI_API_KEY"):
+ raise MissingAPIKeyError(
+ "OPENAI_API_KEY environment variable not set. "
+ "Please set it in agent/.env file. "
+ "Get your API key from: https://platform.openai.com/api-keys"
+ )
capabilities = AgentCapabilities(
streaming=True,
diff --git a/agent/agent.py b/agent/agent.py
index 5283cd88..f3997e45 100644
--- a/agent/agent.py
+++ b/agent/agent.py
@@ -15,10 +15,12 @@
import json
import logging
import os
+import time
from collections.abc import AsyncIterable
from typing import Any
import jsonschema
+import litellm
from google.adk.agents.llm_agent import LlmAgent
from google.adk.artifacts import InMemoryArtifactService
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
@@ -36,6 +38,11 @@
logger = logging.getLogger(__name__)
+# Error handling configuration
+RATE_LIMIT_RETRY_DELAY = 30 # seconds to wait before retrying after rate limit
+MAX_RATE_LIMIT_RETRIES = 3 # maximum number of retries for rate limit errors
+GENERAL_ERROR_RETRY_DELAY = 5 # seconds to wait for general errors
+
AGENT_INSTRUCTION = """
You are a helpful restaurant finding assistant. Your goal is to help users find and book restaurants using a rich UI.
@@ -93,7 +100,7 @@ def get_processing_message(self) -> str:
def _build_agent(self, use_ui: bool) -> LlmAgent:
"""Builds the LLM agent for the restaurant agent."""
- LITELLM_MODEL = os.getenv("LITELLM_MODEL", "gemini/gemini-2.5-flash")
+ LITELLM_MODEL = os.getenv("LITELLM_MODEL", "openrouter/google/gemini-2.0-flash-exp:free")
if use_ui:
# Construct the full prompt with UI instructions, examples, and schema
@@ -133,6 +140,7 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]:
max_retries = 1 # Total 2 attempts
attempt = 0
current_query_text = query
+ rate_limit_retry_count = 0
# Ensure schema was loaded
if self.use_ui and self.a2ui_schema_object is None:
@@ -161,29 +169,163 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]:
)
final_response_content = None
- async for event in self._runner.run_async(
- user_id=self._user_id,
- session_id=session.id,
- new_message=current_message,
- ):
- logger.info(f"Event from runner: {event}")
- if event.is_final_response():
- if (
- event.content
- and event.content.parts
- and event.content.parts[0].text
- ):
- final_response_content = "\n".join(
- [p.text for p in event.content.parts if p.text]
- )
- break # Got the final response, stop consuming events
+ try:
+ async for event in self._runner.run_async(
+ user_id=self._user_id,
+ session_id=session.id,
+ new_message=current_message,
+ ):
+ logger.info(f"Event from runner: {event}")
+ if event.is_final_response():
+ if (
+ event.content
+ and event.content.parts
+ and event.content.parts[0].text
+ ):
+ final_response_content = "\n".join(
+ [p.text for p in event.content.parts if p.text]
+ )
+ break # Got the final response, stop consuming events
+ else:
+ logger.info(f"Intermediate event: {event}")
+ # Yield intermediate updates on every attempt
+ yield {
+ "is_task_complete": False,
+ "updates": self.get_processing_message(),
+ }
+
+ # Reset rate limit counter on success
+ rate_limit_retry_count = 0
+
+ except litellm.RateLimitError as e:
+ rate_limit_retry_count += 1
+ logger.error(
+ f"--- RestaurantAgent.stream: Rate limit error (attempt {rate_limit_retry_count}/{MAX_RATE_LIMIT_RETRIES}): {e} ---"
+ )
+
+ if rate_limit_retry_count <= MAX_RATE_LIMIT_RETRIES:
+ retry_after = RATE_LIMIT_RETRY_DELAY
+
+ # Try to extract retry-after from error message if available
+ error_msg = str(e)
+ if "retry after" in error_msg.lower():
+ try:
+ # Try to parse retry-after time from error message
+ import re
+ match = re.search(r'retry.*?(\d+)', error_msg, re.IGNORECASE)
+ if match:
+ retry_after = int(match.group(1))
+ except:
+ pass
+
+ yield {
+ "is_task_complete": False,
+ "updates": (
+ f"The AI service is currently experiencing high demand. "
+ f"Retrying in {retry_after} seconds... "
+ f"(Attempt {rate_limit_retry_count}/{MAX_RATE_LIMIT_RETRIES})"
+ ),
+ }
+
+ logger.info(f"Waiting {retry_after} seconds before retry...")
+ time.sleep(retry_after)
+
+ # Don't increment attempt counter for rate limits, just retry
+ attempt -= 1
+ continue
else:
- logger.info(f"Intermediate event: {event}")
- # Yield intermediate updates on every attempt
+ logger.error("--- Max rate limit retries exceeded ---")
+ yield {
+ "is_task_complete": True,
+ "content": (
+ "I apologize, but the AI service is currently experiencing very high demand "
+ "and is temporarily rate-limited. Please try again in a few minutes. "
+ "\n\nAlternatively, you can:\n"
+ "1. Wait a few minutes and try again\n"
+ "2. Check if your API provider has rate limit restrictions\n"
+ "3. Consider using a different model in your .env configuration"
+ ),
+ }
+ return
+
+ except litellm.APIConnectionError as e:
+ logger.error(f"--- RestaurantAgent.stream: API connection error: {e} ---")
+ yield {
+ "is_task_complete": True,
+ "content": (
+ "I'm sorry, I'm having trouble connecting to the AI service. "
+ "Please check your internet connection and try again."
+ ),
+ }
+ return
+
+ except litellm.AuthenticationError as e:
+ logger.error(f"--- RestaurantAgent.stream: Authentication error: {e} ---")
+ yield {
+ "is_task_complete": True,
+ "content": (
+ "Authentication error: Please check your API key configuration in the .env file. "
+ "Make sure OPENROUTER_API_KEY or OPENAI_API_KEY is set correctly."
+ ),
+ }
+ return
+
+ except litellm.InvalidRequestError as e:
+ logger.error(f"--- RestaurantAgent.stream: Invalid request error: {e} ---")
+ yield {
+ "is_task_complete": True,
+ "content": (
+ "I'm sorry, there was an issue with the request. "
+ "This might be due to an invalid model configuration or request parameters. "
+ f"Error: {str(e)}"
+ ),
+ }
+ return
+
+ except litellm.ContextWindowExceededError as e:
+ logger.error(f"--- RestaurantAgent.stream: Context window exceeded: {e} ---")
+ yield {
+ "is_task_complete": True,
+ "content": (
+ "I'm sorry, the conversation has become too long for the AI model to process. "
+ "Please start a new conversation or try with a shorter query."
+ ),
+ }
+ return
+
+ except (litellm.APIError, litellm.ServiceUnavailableError, litellm.InternalServerError) as e:
+ logger.error(f"--- RestaurantAgent.stream: API service error: {e} ---")
+
+ if attempt <= max_retries:
yield {
"is_task_complete": False,
- "updates": self.get_processing_message(),
+ "updates": (
+ f"The AI service encountered a temporary error. "
+ f"Retrying in {GENERAL_ERROR_RETRY_DELAY} seconds..."
+ ),
+ }
+ time.sleep(GENERAL_ERROR_RETRY_DELAY)
+ continue
+ else:
+ yield {
+ "is_task_complete": True,
+ "content": (
+ "I'm sorry, the AI service is currently experiencing technical difficulties. "
+ "Please try again in a few moments."
+ ),
}
+ return
+
+ except Exception as e:
+ logger.error(f"--- RestaurantAgent.stream: Unexpected error: {type(e).__name__}: {e} ---")
+ yield {
+ "is_task_complete": True,
+ "content": (
+ f"I'm sorry, an unexpected error occurred: {type(e).__name__}. "
+ "Please try again or contact support if the issue persists."
+ ),
+ }
+ return
if final_response_content is None:
logger.warning(
diff --git a/app/page.tsx b/app/page.tsx
index 3b11f552..d108e466 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -8,21 +8,28 @@ import { theme } from "./theme";
export const dynamic = "force-dynamic";
const A2UIMessageRenderer = createA2UIMessageRenderer({ theme });
-const activityRenderers = [A2UIMessageRenderer];
export default function Home() {
return (