Warning
This version of Vanna is actively under development and may contain breaking changes until it is officially released to PyPI.
To install while in development, use:
pip install --force-reinstall --no-cache-dir 'vanna[flask,anthropic] @ git+https://github.com/vanna-ai/vanna.git@v2'Important
If you're upgrading from an older version of Vanna, use the Migration Guide.
Turn natural language into data insights β with enterprise-grade security baked in
Vanna is purpose-built for data analytics with user awareness as a first-class concern. Drop in our web component, connect your existing auth, and start querying data securely.
graph LR
A[π€ User asks:<br/>'Show me Q4 sales'] --> B[π Identity flows through<br/>every layer]
B --> C[π§° Tools execute securely<br/>with permissions]
C --> D[π Rich UI streams back<br/>tables, charts, code]
style B fill:#FFD93D,stroke:#333,stroke-width:3px
Built for Production Data Analytics
- Pre-built web component + backend β No need to build your own chat UI
- User-aware at every layer β Identity and permissions flow through the entire system
- Rich streaming responses β Tables, charts, SQL code blocks, not just text
- Works with your existing auth β Cookies, JWTs, session tokens
- Enterprise security built-in β Row-level security, audit logs, rate limiting
pip install vanna[anthropic] # or [openai]from vanna import Agent, AgentConfig
from vanna.servers.fastapi import VannaFastAPIServer
from vanna.core.registry import ToolRegistry
from vanna.core.user import UserResolver, User, RequestContext
from vanna.integrations.anthropic import AnthropicLlmService
from vanna.tools import RunSqlTool
from vanna.integrations.sqlite import SqliteRunner
# 1. Define how to resolve users from requests
class SimpleUserResolver(UserResolver):
async def resolve_user(self, request_context: RequestContext) -> User:
# In production, validate cookies/JWTs here
user_id = request_context.get_cookie('user_id') or 'demo_user'
return User(id=user_id, group_memberships=['read_sales'])
# 2. Set up LLM and tools
llm = AnthropicLlmService(model="claude-sonnet-4-5")
tools = ToolRegistry()
tools.register(RunSqlTool(sql_runner=SqliteRunner(database_path="./data.db")))
# 3. Create agent
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=SimpleUserResolver()
)
# 4. Create and run server
server = VannaFastAPIServer(agent)
app = server.create_app()
# Run with: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
# Visit http://localhost:8000 to see the web UIEvery layer of the system knows who the user is and what they can access.
sequenceDiagram
participant U as π€ User
participant W as π Web Component
participant S as π Flask/FastAPI
participant R as πͺͺ User Resolver
participant A as π€ Agent
participant T as π§° Tools
U->>W: "Show Q4 sales"
W->>S: POST /api/vanna/v2/chat_sse
S->>R: Extract user identity
R->>A: User(id=alice, group_memberships=[read_sales])
A->>A: Generate personalized system prompt
A->>T: Execute SQL tool (user-aware)
T->>T: Apply row-level security
T->>A: Return filtered results
A->>W: Stream: Table β Chart β Summary
W->>U: Display results
Not just authentication β authorization at every step:
- System prompt customized per user
- Tools check permissions before execution
- SQL queries filtered by row-level security
- Audit logs per user
- Rate limiting per user
<!-- Works with any existing app -->
<vanna-chat
api-endpoint="/api/vanna/v2/chat_sse"
initial-message="What can I help you with?"
theme="dark">
</vanna-chat>Features:
- Uses your existing cookies/JWTs (no new login system)
- Renders streaming tables, charts, SQL code blocks
- Responsive and customizable
- Framework-agnostic (works with React, Vue, plain HTML)
Out-of-the-box tools:
- SQL generation and execution (with user permissions)
- Data visualization with Plotly
- File system operations (for coding agents)
- Python code execution (sandboxed)
from vanna.tools import RunSqlTool, VisualizeDataTool
from vanna.integrations.sqlite import SqliteRunner
from vanna.integrations.local import LocalFileSystem
file_system = LocalFileSystem("./data")
tools.register(RunSqlTool(
sql_runner=SqliteRunner(database_path="./data.db"),
file_system=file_system
))
tools.register(VisualizeDataTool(file_system=file_system))| Feature | Description |
|---|---|
| Row-Level Security | SQL tools respect database permissions |
| Audit Logs | Every query and tool call logged per user |
| Rate Limiting | Per-user token/request limits via lifecycle hooks |
| Observability | Built-in tracing and debugging hooks |
| Conversation Management | Persistent conversation storage |
| Content Filtering | Extensible filtering system |
graph TB
subgraph Frontend["π Frontend"]
WC[Web Component<br/><vanna-chat>]
end
subgraph Backend["π Python Server (Flask/FastAPI)"]
direction TB
SSE[SSE/WebSocket Handler]
end
subgraph Agent["π€ User-Aware Agent"]
direction TB
UR[πͺͺ User Resolver<br/>YOUR auth system]
SP[βοΈ System Prompt<br/>Per-user customization]
LLM[π§ LLM<br/>Claude/GPT/Gemini]
Tools[π§° Tools<br/>SQL, Charts, Memory]
Comp[π UI Components<br/>Tables, Charts, Code]
UR --> SP
SP --> LLM
UR --> Tools
Tools <--> LLM
Tools --> Comp
LLM --> Comp
end
WC -->|User question<br/>+ cookies/JWT| SSE
SSE -->|Request context| UR
Comp -->|Streaming components| SSE
SSE -->|Progressive updates| WC
style UR fill:#95E1D3
style Tools fill:#FFD93D
style Comp fill:#C7F1FF
1. User Resolver β You define this!
from vanna.core.user import UserResolver, User, RequestContext
class MyUserResolver(UserResolver):
async def resolve_user(self, request_context: RequestContext) -> User:
# Extract from your existing auth system
token = request_context.get_header('Authorization')
user_data = self.decode_jwt(token) # Your logic
return User(
id=user_data['id'],
email=user_data['email'],
group_memberships=user_data['groups'], # Key!
metadata={'role': user_data['role']}
)2. User-Aware Tools β Check permissions automatically
from vanna.core.tool import Tool, ToolContext, ToolResult
from pydantic import BaseModel, Field
from typing import Type
class QueryArgs(BaseModel):
query: str = Field(description="SQL query to execute")
class CustomSQLTool(Tool[QueryArgs]):
@property
def name(self) -> str:
return "query_database"
@property
def description(self) -> str:
return "Execute a SQL query against the database"
@property
def access_groups(self) -> list[str]:
return ["read_sales"] # Only users in this group can use this tool
def get_args_schema(self) -> Type[QueryArgs]:
return QueryArgs
async def execute(self, context: ToolContext, args: QueryArgs) -> ToolResult:
user = context.user # Automatically injected
# Apply row-level security
filtered_query = self.add_user_filters(args.query, user)
results = await self.db.execute(filtered_query)
return ToolResult(
success=True,
result_for_llm=str(results)
)3. Streaming UI Components
async for component in agent.send_message(request_context=ctx, message="Show sales"):
# Rich component: structured data (tables, charts, status cards)
print(type(component.rich_component).__name__)
# Simple component: plain text fallback
print(component.simple_component.text)Output:
StatusBarUpdateComponent # "Processing..."
TaskTrackerUpdateComponent # "Load conversation context"
RichTextComponent # "Let me query the sales data..."
StatusCardComponent # "Executing run_sql"
DataFrameComponent # Tabular results
RichTextComponent # "Here are your top customers..."
If you already have a FastAPI application, you can add Vanna as additional routes:
from fastapi import FastAPI
from vanna import Agent, AgentConfig
from vanna.servers.base import ChatHandler
from vanna.servers.fastapi.routes import register_chat_routes
from vanna.core.registry import ToolRegistry
from vanna.core.user import UserResolver, User, RequestContext
from vanna.integrations.anthropic import AnthropicLlmService
from vanna.tools import RunSqlTool
from vanna.integrations.sqlite import SqliteRunner
# Your existing FastAPI app
app = FastAPI()
# Your existing routes
@app.get('/api/users')
async def get_users():
return {'users': [...]}
@app.get('/api/products')
async def get_products():
return {'products': [...]}
# Add Vanna agent
class CookieUserResolver(UserResolver):
async def resolve_user(self, request_context: RequestContext) -> User:
user_id = request_context.get_cookie('user_id') or 'anonymous'
role = request_context.get_cookie('role') or 'guest'
groups = []
if role == 'admin':
groups = ['read_sales', 'read_confidential', 'admin']
elif role == 'analyst':
groups = ['read_sales']
return User(id=user_id, group_memberships=groups)
# Set up agent
llm = AnthropicLlmService(model="claude-sonnet-4-5")
tools = ToolRegistry()
tools.register(RunSqlTool(sql_runner=SqliteRunner(database_path="./data.db")))
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=CookieUserResolver()
)
# Add Vanna routes to your existing app
chat_handler = ChatHandler(agent)
register_chat_routes(app, chat_handler)
# Run with: uvicorn main:app --host 0.0.0.0 --port 8000 --reloadThis adds these endpoints to your existing FastAPI app:
GET /- Vanna web component UI (you may want to change this)POST /api/vanna/v2/chat_sse- Server-Sent Events streamingPOST /api/vanna/v2/chat_poll- Polling endpointGET /health- Health check
To customize the routes or serve the UI at a different path, see the server configuration docs.
Create custom tools by extending the Tool base class:
from vanna.core.tool import Tool, ToolContext, ToolResult
from vanna.components import UiComponent, NotificationComponent, SimpleTextComponent, ComponentType
from pydantic import BaseModel, Field
from typing import Type
class EmailArgs(BaseModel):
recipient: str = Field(description="Email recipient")
subject: str = Field(description="Email subject")
body: str = Field(description="Email body")
class EmailTool(Tool[EmailArgs]):
@property
def name(self) -> str:
return "send_email"
@property
def description(self) -> str:
return "Send an email to a user"
@property
def access_groups(self) -> list[str]:
return ["send_email"] # Only users in this group can use this tool
def get_args_schema(self) -> Type[EmailArgs]:
return EmailArgs
async def execute(self, context: ToolContext, args: EmailArgs) -> ToolResult:
user = context.user
# Check domain restrictions
if not args.recipient.endswith('@company.com'):
error_msg = "Can only send to company email addresses"
return ToolResult(
success=False,
result_for_llm=error_msg,
error=error_msg,
ui_component=UiComponent(
rich_component=NotificationComponent(
type=ComponentType.NOTIFICATION,
level="error",
message=error_msg
),
simple_component=SimpleTextComponent(text=error_msg)
)
)
# Send email (your logic)
await self.email_service.send(
from_email=user.email,
to=args.recipient,
subject=args.subject,
body=args.body
)
success_msg = f"Email sent to {args.recipient}"
return ToolResult(
success=True,
result_for_llm=success_msg,
ui_component=UiComponent(
rich_component=NotificationComponent(
type=ComponentType.NOTIFICATION,
level="success",
message=success_msg
),
simple_component=SimpleTextComponent(text=success_msg)
)
)
# Register tool
tools.register(EmailTool())from vanna import AgentConfig
config = AgentConfig(
max_tool_iterations=10, # Max tool calls per message
stream_responses=True, # Enable streaming
temperature=0.7, # LLM temperature
include_thinking_indicators=True, # Show "Thinking..." states
auto_save_conversations=True, # Auto-persist conversations
max_tokens=None # Maximum response tokens
)
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=user_resolver,
config=config
)# Anthropic
export ANTHROPIC_API_KEY="sk-ant-..."
export ANTHROPIC_MODEL="claude-sonnet-4-5"
# OpenAI
export OPENAI_API_KEY="sk-..."
export OPENAI_MODEL="gpt-5"
# Database
export DATABASE_URL="postgresql://localhost/mydb"from vanna.integrations.local import MemoryConversationStore
# Use in-memory storage (default)
store = MemoryConversationStore()
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=user_resolver,
conversation_store=store
)
# List user's conversations
alice = User(id="alice")
conversations = await store.list_conversations(user=alice)
# Get conversation history
conversation = await store.get_conversation(
conversation_id="conv_123",
user=alice
)from vanna.core.lifecycle import LifecycleHook
class QuotaCheckHook(LifecycleHook):
async def before_message(self, user: User, message: str) -> str:
# Check if user has quota remaining
if not await self.check_quota(user.id):
raise Exception("Quota exceeded")
return message
async def after_tool(self, result: ToolResult) -> ToolResult:
# Log tool execution
await self.log_tool_execution(result)
return result
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=user_resolver,
lifecycle_hooks=[QuotaCheckHook()]
)from vanna.core.middleware import LlmMiddleware
from vanna.core.llm import LlmRequest, LlmResponse
class CachingMiddleware(LlmMiddleware):
async def before_llm_request(self, request: LlmRequest) -> LlmRequest:
# Check cache before sending to LLM
cached = await self.cache.get(request)
if cached:
return cached
return request
async def after_llm_response(
self,
request: LlmRequest,
response: LlmResponse
) -> LlmResponse:
# Cache the response
await self.cache.set(request, response)
return response
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=user_resolver,
llm_middlewares=[CachingMiddleware()]
)from vanna.core.observability import ObservabilityProvider
class LoggingProvider(ObservabilityProvider):
async def create_span(self, name: str, attributes: dict):
print(f"Starting: {name}")
return Span(name, attributes)
async def record_metric(self, name: str, value: float, unit: str, tags: dict):
print(f"Metric: {name} = {value}{unit}")
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=user_resolver,
observability_provider=LoggingProvider()
)from vanna.core.enricher import ContextEnricher
from vanna.core.tool import ToolContext
class UserMetadataEnricher(ContextEnricher):
async def enrich_context(self, context: ToolContext) -> ToolContext:
# Add additional user metadata from database
user_metadata = await self.db.get_user_metadata(context.user.id)
context.user.metadata.update(user_metadata)
return context
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=user_resolver,
context_enrichers=[UserMetadataEnricher()]
)Vanna is ideal for:
- Building data analytics applications with natural language interfaces
- Applications requiring user-aware permissions throughout
- Teams that want a pre-built web component + backend integration
- Enterprise environments with strict security requirements
- Use cases needing rich streaming responses (tables, charts, SQL)
- Integrating with existing authentication systems
- Migration Guide: Migrating from Vanna 1.x to 2.0+
- GitHub Discussions: GitHub Discussions
- Email: support@vanna.ai