Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
115 changes: 113 additions & 2 deletions py-llamaindex/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,114 @@
# Assistant0: An AI Personal Assistant Secured with Auth0 - LlamaIndex Python Version
# Assistant0: An AI Personal Assistant Secured with Auth0 - Llamaindex Python/FastAPI Version

Assistant0 an AI personal assistant that consolidates your digital life by dynamically accessing multiple tools to help you stay organized and efficient.

## About the template

This template scaffolds an Auth0 + LlamaIndex.js + React JS starter app. It mainly uses the following libraries:

- [LlamaIndex's Python framework](https://docs.llamaindex.ai/en/stable/#introduction)
- The [Auth0 AI SDK](https://github.com/auth0-lab/auth0-ai-python) and [Auth0 FastAPI SDK](https://github.com/auth0/auth0-fastapi) to secure the application and call third-party APIs.
- [Auth0 FGA](https://auth0.com/fine-grained-authorization) to define fine-grained access control policies for your tools and RAG pipelines.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also highlight that we are using a lot of Langgraph libraries / sdks w/ this example now? Or is that still t.b.d.?

If we go w/ Langgraph, just wanting to call it out better in README. Also agree w/ @deepu105 's feedback, that if we already have a langgraph sample, capturing some of the additional things in the existing example:
https://github.com/auth0-samples/auth0-assistant0/tree/main/py-langchain


## 🚀 Getting Started

First, clone this repo and download it locally.

```bash
git clone https://github.com/auth0-samples/auth0-assistant0.git
cd auth0-assistant0/py-llamaindex
```

The project is divided into two parts:

- `backend/` contains the backend code for the Web app and API written in Python using FastAPI.
- `frontend/` contains the frontend code for the Web app written in React as a Vite SPA.

### Setup the backend

```bash
cd backend
```

Next, you'll need to set up environment variables in your repo's `.env` file. Copy the `.env.example` file to `.env`.

To start with the basic examples, you'll just need to add your OpenAI API key and Auth0 credentials.

- To start with the examples, you'll just need to add your OpenAI API key and Auth0 credentials for the Web app.
- You can setup a new Auth0 tenant with an Auth0 Web App and Token Vault following the Prerequisites instructions [here](https://auth0.com/ai/docs/call-others-apis-on-users-behalf).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we describe some place how you are configuring this app with Auth? E.g. "Create an API..", "Create a SPA application"... etc.

I'm having trouble following this at the moment, and it's quite important as far as what auth flow we want to use.

If going for an Auth0 API/resource server for the Fast API + embedded llamaindex agent, and a SPA application for the client, we'll like want to make use of the new access token / token vault flow. (more like this example w/ a SPA: auth0/docs-v2#39)

If going for a FastAPI (app) (Regular Web App for the Auth0 client) like the existing example here, and an embedded llamaindex agent, we'll likely be fine w/ the existing refresh token / token vault exchange flow.

If going for a FastAPI (app) (Regular Web App for the Auth0 client) like the existing example here, and an external llamaindex agent/external LangGraph server, we'll likely want access token / token vault exchange flow (and the AT provided to Langgraph server).

- An Auth0 FGA account, you can create one [here](https://dashboard.fga.dev). Add the FGA store ID, client ID, client secret, and API URL to the `.env` file.

Next, install the required packages using your preferred package manager, e.g. uv:

```bash
uv sync
```

Now you're ready to start the database:

```bash
# start the postgres database
docker compose up -d
```

Initialize FGA store:

```bash
source .venv/bin/activate
python -m app.core.fga_init
```

Now you're ready to run the development server:

```bash
source .venv/bin/activate
uv run uvicorn app.main:app --reload
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't is fastapi dev app/main.py

# fastapi dev app/main.py
```

### Start the frontend server

Rename `.env.example` file to `.env` in the `frontend` directory.

Finally, you can start the frontend server in another terminal:

```bash
cd frontend
cp .env.example .env # Copy the `.env.example` file to `.env`.
npm install
npm run dev
```

## Auth configuration

There are two supported setups:

### 1) SPA (frontend) + FastAPI (backend) + embedded LlamaIndex agent
- In Auth0 Dashboard:
- Create a **SPA Application** for the frontend.
- Create an **API (Resource Server)** for the FastAPI backend.
- (If using Federated Connections like Google Calendar) enable **Token Vault** and grant your backend the right audience/scopes.
- The frontend obtains an **access token** for the API and calls the FastAPI endpoints.
- FastAPI uses **Auth0 FastAPI SDK** to validate the token and manage the session. Tools read access tokens from the session and use Token Vault for federated access.

### 2) Regular Web App (FastAPI handles browser auth) + embedded LlamaIndex agent
- In Auth0 Dashboard:
- Create a **Regular Web App** for FastAPI.
- (Optional) Create an API if you also expose protected endpoints to SPAs.
- FastAPI handles cookie-based session and federated connections via Token Vault.
- Tools do **not** receive tokens as arguments; they read them from the session or use the federated-connection wrapper.
Copy link

@priley86 priley86 Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super helpful 👍 👍

Seems this will work ok and RT -> AT / token exchange flow is desired if Regular Web App is used, and agent is embedded as we are here. I'm good w/ this if others are 👍


This will start a React vite server on port 5173.

#TODO IMAGE

Agent configuration lives in `backend/app/agents/assistant0.ts`. From here, you can change the prompt and model, or add other tools and logic.

## License

This project is open-sourced under the MIT License - see the [LICENSE](LICENSE) file for details.

## Author

This project is built by [Adam W.](https://github.com/AdamWozniewski).

Coming soon
24 changes: 24 additions & 0 deletions py-llamaindex/backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
APP_BASE_URL='http://localhost:8000'
API_PREFIX=/api

AUTH0_SECRET='use [openssl rand -hex 32] to generate a 32 bytes value'
AUTH0_DOMAIN=''
AUTH0_CLIENT_ID=''
AUTH0_CLIENT_SECRET=''

OPENAI_API_KEY=''
PROVIDER=openai
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this used?


# Database
DATABASE_URL="postgresql+psycopg://postgres:postgres@localhost:5432/ai_documents_db"

# Auth0 FGA
FGA_STORE_ID=<your-fga-store-id>
FGA_CLIENT_ID=<your-fga-store-client-id>
FGA_CLIENT_SECRET=<your-fga-store-client-secret>
FGA_API_URL=https://api.xxx.fga.dev
FGA_API_AUDIENCE=https://api.xxx.fga.dev/

# Shop API URL (Optional)
# SHOP_API_URL="http://localhost:3001/api/shop"
# SHOP_API_AUDIENCE="https://api.shop-online-demo.com"
10 changes: 10 additions & 0 deletions py-llamaindex/backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
__pycache__
app.egg-info
*.pyc
.mypy_cache
.coverage
htmlcov
.cache
.venv
.env

1 change: 1 addition & 0 deletions py-llamaindex/backend/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
41 changes: 41 additions & 0 deletions py-llamaindex/backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Setup the backend

```bash
cd backend
```

You'll need to set up environment variables in your repo's `.env` file. Copy the `.env.example` file to `.env`.

To start with the basic examples, you'll just need to add your OpenAI API key and Auth0 credentials.

- To start with the examples, you'll just need to add your OpenAI API key and Auth0 credentials for the Web app.
- You can setup a new Auth0 tenant with an Auth0 Web App and Token Vault following the Prerequisites instructions [here](https://auth0.com/ai/docs/call-others-apis-on-users-behalf).
- An Auth0 FGA account, you can create one [here](https://dashboard.fga.dev). Add the FGA store ID, client ID, client secret, and API URL to the `.env` file.

Next, install the required packages using your preferred package manager, e.g. uv:

```bash
uv sync
```

Now you're ready to start and migrate the database:

```bash
# start the postgres database
docker compose up -d
```

Initialize FGA store:

```bash
source .venv/bin/activate
python -m app.core.fga_init
```

Now you're ready to run the development server:

```bash
source .venv/bin/activate
uv run uvicorn app.main:app --reload
# fastapi dev app/main.py
```
Empty file.
Empty file.
28 changes: 28 additions & 0 deletions py-llamaindex/backend/app/agents/assistant0_li.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations
from llama_index.core.agent import ReActAgentWorker
from llama_index.llms.openai import OpenAI

from app.agents.tools.user_info_li import get_user_info_li
from app.agents.tools.google_calendar_li import list_upcoming_events_li
from app.agents.tools.shop_online_li import shop_online_li
from app.agents.tools.context_docs_li import get_context_docs_li

llm = OpenAI(model="gpt-4.1-mini", temperature=0.2)

tools = [
get_user_info_li,
list_upcoming_events_li,
shop_online_li,
get_context_docs_li,
]

agent = ReActAgentWorker.from_tools(
tools=tools,
llm=llm,
verbose=True,
system_prompt=(
"You are a personal assistant named Assistant0. "
"Use tools when helpful; prefer get_context_docs for knowledge base queries. "
"Render email-like bodies as markdown (no code fences)."
),
).as_agent()
Comment on lines +19 to +28

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are you finding the interrupt support w/ this ReActAgent worker, and compatibility w/ langgraph's useStream hook? Would it be better to use separate langgraph-cli server w/ access token exchange flow in this case?

It seems a simpler use case may also be like this one.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, i am finding a lot of gaps w/ createReactAgent on the typescript side (and not sure how this is behaving on the python side). Ideally we can also support interrupt flows for step-up auth 🤞 . If using useStream hook from langgraph-sdk on the client, the langgraph-cli approach may be easier b/c it comes w/ langgraph protocol fully implemented. If sticking w/ this approach and embedded agent w/ ReActAgent, you may find it easier to explore a custom SSE stream (and replace useStream hook on the client w/ custom handlers), example. Leave this to you though to determine as to how to reflect.

Empty file.
38 changes: 38 additions & 0 deletions py-llamaindex/backend/app/agents/tools/context_docs_li.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations
from typing import List

from llama_index.core.tools import FunctionTool
from pydantic import BaseModel

from app.core.rag_li import retrieve_nodes
from app.core.fga import authorization_manager


class GetContextDocsSchema(BaseModel):
question: str

async def get_context_docs_li_fn(question: str, user_email: str) -> str:
nodes = retrieve_nodes(question, top_k=12)

allowed: List[str] = []
for n in nodes:
doc_id = n.metadata.get("document_id")
if not doc_id:
continue

can_view = await authorization_manager.check_relation(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user=user_email, doc_id=doc_id, relation="can_view"
)
if can_view:
allowed.append(n.get_content(metadata_mode="none"))

if not allowed:
return "I couldn't find any documents you are allowed to view."
return "\n\n".join(allowed)


get_context_docs_li = FunctionTool.from_defaults(
name="get_context_docs",
description="Retrieve documents from the knowledge base (LlamaIndex + FGA).",
fn=get_context_docs_li_fn,
)
44 changes: 44 additions & 0 deletions py-llamaindex/backend/app/agents/tools/google_calendar_li.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations
import datetime, json
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from llama_index.core.tools import FunctionTool
from auth0_ai_llamaindex.federated_connections import (
with_federated_connection,
get_access_token_for_connection,
Copy link

@priley86 priley86 Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Believe this library is currently using "refresh_token" exchange. We should enhance this library in the core to support subject_token_type urn:ietf:params:oauth:token-type:access_token / access token exchange as well:

https://github.com/auth0-lab/auth0-ai-python/blob/main/packages/auth0-ai/auth0_ai/authorizers/federated_connection_authorizer.py#L192-L197

This will likely be useful in the case that LangGraph server is hosted separately.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^ we have a separate ticket for this effort this sprint. If we keep the langgraph agent embedded, the RT flow here now is fine. If the langgraph server / agent is separate server, my current assumption is we should use AT flow and add that support to the core.

)

async def _list_events() -> str:
google_access_token = get_access_token_for_connection()
if not google_access_token:
return "Authorization required to access the Federated Connection API"

service = build("calendar", "v3", credentials=Credentials(google_access_token))
events = (service.events()
.list(
calendarId="primary",
timeMin=datetime.datetime.utcnow().isoformat() + "Z",
timeMax=(datetime.datetime.utcnow() + datetime.timedelta(days=7)).isoformat() + "Z",
maxResults=5,
singleEvents=True,
orderBy="startTime",
)
.execute()
.get("items", [])
)

return json.dumps([
{"summary": e.get("summary", "(no title)"),
"start": e["start"].get("dateTime", e["start"].get("date"))}
for e in events
])

list_upcoming_events_li = with_federated_connection(
FunctionTool.from_defaults(
name="list_upcoming_events",
description="List upcoming events from the user's Google Calendar.",
fn=_list_events,
),
connection="google-oauth2",
scopes=["https://www.googleapis.com/auth/calendar.events"],
)
12 changes: 12 additions & 0 deletions py-llamaindex/backend/app/agents/tools/shop_online_li.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from __future__ import annotations
from typing import Dict, Any
from llama_index.core.tools import FunctionTool

async def _shop_online(product: str, quantity: int) -> Dict[str, Any]:
return {"ok": True, "message": f"Would buy {quantity} x {product} (demo stub)"}

shop_online_li = FunctionTool.from_defaults(
name="shop_online",
description="Demo purchase tool (stub).",
fn=_shop_online,
)
26 changes: 26 additions & 0 deletions py-llamaindex/backend/app/agents/tools/user_info_li.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations
import httpx
from llama_index.core.tools import FunctionTool
from app.core.config import settings
from app.core.auth import auth_client

async def _get_user_info() -> str:
if not sess:
return "There is no user logged in."

access_token = sess.get("token_sets", [{}])[0].get("access_token")
if not access_token:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not get access token from user session

return "There is no user logged in."

async with httpx.AsyncClient() as client:
r = await client.get(
f"https://{settings.AUTH0_DOMAIN}/userinfo",
headers={"Authorization": f"Bearer {access_token}"},
)
return f"User information: {r.json()}" if r.status_code == 200 else "I couldn't verify your identity"

get_user_info_li = FunctionTool.from_defaults(
name="get_user_info",
description="Get information about the current logged in user.",
fn=_get_user_info,
)
Empty file.
10 changes: 10 additions & 0 deletions py-llamaindex/backend/app/api/api_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from fastapi import APIRouter
from app.api.routes.agent_li import agent_router
from app.api.routes.documents import documents_router
from app.core.auth import auth_router

api_router = APIRouter()

api_router.include_router(agent_router)
api_router.include_router(auth_router, tags=["auth"])
api_router.include_router(documents_router)
Empty file.
23 changes: 23 additions & 0 deletions py-llamaindex/backend/app/api/routes/agent_li.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, Request
from fastapi.responses import StreamingResponse, JSONResponse
from app.core.auth import auth_client
from app.agents.assistant0_li import agent

agent_router = APIRouter(prefix="/agent", tags=["agent"])

@agent_router.post("/chat")
async def chat(request: Request, auth_session=Depends(auth_client.require_session)):
try:
body = await request.json()
query: str = body.get("input") or body.get("message") or ""
stream = agent.stream_chat(query)

async def gen():
async for ev in stream:
if token := ev.get("delta"):
yield token
return StreamingResponse(gen(), media_type="text/plain")

except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
Loading