Skip to content
Merged
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
23 changes: 23 additions & 0 deletions Config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"graphs": {
"agent": "graph.react:app",
"checkpointer": "graph.react:checkpointer",
"store": "graph.store:store",
"container": null
},
"env": ".env",
"auth": null,
"redis": "redis://localhost:6379/0"
}

# Supported Auth Methods
- "auth": null
- "auth": "jwt" # JWT-based authentication
it will check `JWT_SECRET_KEY` and `JWT_ALGORITHM` in environment variables
- "auth": "custom" # Custom authentication, in this case you need to implement your own auth logic and share the file path here
```
auth: {
method: "custom",
path: "/path/to/your/auth_module.py",
user_id_mapping: "user_id" # Optional, default is "user_id"
}
File renamed without changes.
7 changes: 4 additions & 3 deletions pyagenity.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"graphs": {
"agent": "graph.react:app",
"checkpointer": "graph.react:checkpointer",
"container": null
"injectq": null
},
"env": ".env",
"auth": null
"auth": null,
"thread_model_name": "gemini/gemini-2.0-flash",
"generate_thread_name": false
}
13 changes: 12 additions & 1 deletion pyagenity_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
"""Pyagenity API - A Python API framework with GraphQL support and task management."""
"""Pyagenity API - A Python API framework For Pyagenity Graphs."""

__version__ = "1.0.0"
__author__ = "Shudipto Trafder"
__email__ = "shudiptotrafder@gmail.com"


# Lets expose few things the user suppose to use
from .src.app.core.auth.base_auth import BaseAuth
from .src.app.utils.snowflake_id_generator import SnowFlakeIdGenerator


__all__ = [
"BaseAuth",
"SnowFlakeIdGenerator",
]
21 changes: 14 additions & 7 deletions pyagenity_api/src/app/core/auth/auth_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,30 @@

from fastapi import Depends, Response
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from injectq.integrations import InjectAPI

from pyagenity_api.src.app.core.config.settings import get_settings

from .authentication import verify_jwt
from pyagenity_api.src.app.core import logger
from pyagenity_api.src.app.core.auth.base_auth import BaseAuth
from pyagenity_api.src.app.core.config.graph_config import GraphConfig


def verify_current_user(
res: Response,
credential: HTTPAuthorizationCredentials = Depends(
HTTPBearer(auto_error=False),
),
config: GraphConfig = InjectAPI(GraphConfig),
auth_backend: BaseAuth = InjectAPI(BaseAuth),
) -> dict[str, Any]:
# check auth backend
user = {}
settings = get_settings()
backend = config.auth_config()
if not backend:
return user

if not auth_backend:
logger.error("Auth backend is not configured")
return user

if settings.AUTH_BACKEND == "jwt":
# now check keys
user = verify_jwt(res, credential)
user = auth_backend.authenticate(res, credential)
return user or {}
70 changes: 0 additions & 70 deletions pyagenity_api/src/app/core/auth/authentication.py

This file was deleted.

24 changes: 24 additions & 0 deletions pyagenity_api/src/app/core/auth/base_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from abc import ABC, abstractmethod
from typing import Any

from fastapi import Response
from fastapi.security import HTTPAuthorizationCredentials


class BaseAuth(ABC):
@abstractmethod
def authenticate(
self, res: Response, credential: HTTPAuthorizationCredentials
) -> dict[str, Any] | None:
"""Authenticate the user based on the provided credentials.
IT should return an empty dict if no authentication is required.
If authentication fails, it should raise an appropriate exception.
In case authentication is successful, it should return a dict with user information,
containing at least a 'user_id' key, and optionally other user details.
Example:
return {"user_id": "12345", "username": "johndoe", "email": "johndoe@example.com"}

What ever keys are returned that will be merged with config in the main graph app.
"""

raise NotImplementedError
78 changes: 78 additions & 0 deletions pyagenity_api/src/app/core/auth/jwt_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os
from typing import Any

import jwt
from fastapi import Response
from fastapi.security import HTTPAuthorizationCredentials

from pyagenity_api.src.app.core import logger
from pyagenity_api.src.app.core.auth.base_auth import BaseAuth
from pyagenity_api.src.app.core.exceptions import UserAccountError


class JwtAuth(BaseAuth):
def authenticate(
self, res: Response, credential: HTTPAuthorizationCredentials
) -> dict[str, Any] | None:
"""No authentication is required, so return None."""
"""
Get the current user based on the provided HTTP
Authorization credentials.

Args:
res (Response): The response object to set headers if needed.
credential (HTTPAuthorizationCredentials): The HTTP Authorization
credentials obtained from the request.

Returns:
UserSchema: A UserSchema object containing the decoded user information.

Raises:
HTTPException: If the credentials are missing.
UserAccountError: If there are token verification errors such as
RevokedIdTokenError,
UserDisabledError,
InvalidIdTokenError,
or any other unexpected exceptions.
"""

if credential is None:
raise UserAccountError(
message="Invalid token, please login again",
error_code="REVOKED_TOKEN",
)
jwt_secret_key = os.environ.get("JWT_SECRET_KEY", None)
jwt_algorithm = os.environ.get("JWT_ALGORITHM", None)

if jwt_secret_key is None or jwt_algorithm is None:
raise UserAccountError(
message="JWT settings are not configured",
error_code="JWT_SETTINGS_NOT_CONFIGURED",
)

try:
decoded_token = jwt.decode(
credential.credentials,
jwt_secret_key, # type: ignore
algorithms=[jwt_algorithm], # type: ignore
)
except jwt.ExpiredSignatureError:
raise UserAccountError(
message="Token has expired, please login again",
error_code="EXPIRED_TOKEN",
)
except jwt.InvalidTokenError as err:
logger.exception("JWT AUTH ERROR", exc_info=err)
raise UserAccountError(
message="Invalid token, please login again",
error_code="INVALID_TOKEN",
)
res.headers["WWW-Authenticate"] = 'Bearer realm="auth_required"'

# check if user_id exists in the token
if "user_id" not in decoded_token:
raise UserAccountError(
message="Invalid token, user_id missing",
error_code="INVALID_TOKEN",
)
return decoded_token
48 changes: 48 additions & 0 deletions pyagenity_api/src/app/core/config/graph_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os
from pathlib import Path

from dotenv import load_dotenv
Expand Down Expand Up @@ -29,9 +30,56 @@ def checkpointer_path(self) -> str | None:
return graphs["checkpointer"]
return None

@property
def injectq_path(self) -> str | None:
graphs = self.data.get("graphs", {})
if "injectq" in graphs:
return graphs["injectq"]
return None

@property
def store_path(self) -> str | None:
graphs = self.data.get("graphs", {})
if "store" in graphs:
return graphs["store"]
return None

@property
def redis_url(self) -> str | None:
return self.data.get("redis", None)

def auth_config(self) -> dict | None:
res = self.data.get("auth", None)
if not res:
return None

if isinstance(res, str) and "jwt" in res:
# Now check jwt secrect and algorithm available in env
secret = os.environ.get("JWT_SECRET_KEY", None)
algorithm = os.environ.get("JWT_ALGORITHM", None)
if not secret or not algorithm:
raise ValueError(
"JWT_SECRET_KEY and JWT_ALGORITHM must be set in environment variables",
)
return {
"method": "jwt",
}

if isinstance(res, dict):
method = res.get("method", None)
path = res.get("path", None)
if method == "custom" and path and Path(path).exists():
return {
"method": "custom",
"path": path,
}

raise ValueError(f"Unsupported auth method: {res}")

@property
def generate_thread_name(self) -> bool:
return self.data.get("generate_thread_name", False)

@property
def thread_model_name(self) -> str | None:
return self.data.get("thread_model_name", None)
44 changes: 25 additions & 19 deletions pyagenity_api/src/app/core/config/sentry_config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import sentry_sdk
from fastapi import Depends
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration

from pyagenity_api.src.app.core import Settings, get_settings, logger

Expand All @@ -20,19 +17,28 @@ def init_sentry(settings: Settings = Depends(get_settings)):
Returns:
None
"""
sentry_sdk.init(
dsn=settings.SENTRY_DSN,
integrations=[
FastApiIntegration(
transaction_style="endpoint",
failed_request_status_codes=[403, range(500, 599)],
),
StarletteIntegration(
transaction_style="endpoint",
failed_request_status_codes=[403, range(500, 599)],
),
],
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
)
logger.debug("Sentry initialized")
try:
import sentry_sdk # noqa: PLC0415
from sentry_sdk.integrations.fastapi import FastApiIntegration # noqa: PLC0415
from sentry_sdk.integrations.starlette import StarletteIntegration # noqa: PLC0415

sentry_sdk.init(
dsn=settings.SENTRY_DSN,
integrations=[
FastApiIntegration(
transaction_style="endpoint",
failed_request_status_codes=[403, range(500, 599)],
),
StarletteIntegration(
transaction_style="endpoint",
failed_request_status_codes=[403, range(500, 599)],
),
],
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
)
logger.debug("Sentry initialized")
except ImportError:
logger.warning("sentry_sdk is not installed, Please install it to use Sentry")
except Exception as e:
logger.warning(f"Error initializing Sentry: {e}")
Loading