pyvand 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyvand-0.1.0/PKG-INFO +71 -0
- pyvand-0.1.0/README.md +44 -0
- pyvand-0.1.0/pyproject.toml +50 -0
- pyvand-0.1.0/setup.cfg +4 -0
- pyvand-0.1.0/src/pyvand/__init__.py +2 -0
- pyvand-0.1.0/src/pyvand/agents/__init__.py +4 -0
- pyvand-0.1.0/src/pyvand/agents/default_agent.py +51 -0
- pyvand-0.1.0/src/pyvand/agents/deps.py +38 -0
- pyvand-0.1.0/src/pyvand/agents/events.py +16 -0
- pyvand-0.1.0/src/pyvand/agents/title_agent.py +62 -0
- pyvand-0.1.0/src/pyvand/agents/utils.py +24 -0
- pyvand-0.1.0/src/pyvand/api/dependencies.py +133 -0
- pyvand-0.1.0/src/pyvand/api/inputs.py +67 -0
- pyvand-0.1.0/src/pyvand/api/job_manager.py +144 -0
- pyvand-0.1.0/src/pyvand/api/router.py +12 -0
- pyvand-0.1.0/src/pyvand/api/routes/admin.py +754 -0
- pyvand-0.1.0/src/pyvand/api/routes/auth.py +180 -0
- pyvand-0.1.0/src/pyvand/api/routes/chat.py +624 -0
- pyvand-0.1.0/src/pyvand/api/routes/config.py +18 -0
- pyvand-0.1.0/src/pyvand/api/routes/users.py +33 -0
- pyvand-0.1.0/src/pyvand/api/security.py +49 -0
- pyvand-0.1.0/src/pyvand/constants.py +68 -0
- pyvand-0.1.0/src/pyvand/database.py +766 -0
- pyvand-0.1.0/src/pyvand/enums.py +12 -0
- pyvand-0.1.0/src/pyvand/factory.py +136 -0
- pyvand-0.1.0/src/pyvand/storage.py +69 -0
- pyvand-0.1.0/src/pyvand.egg-info/PKG-INFO +71 -0
- pyvand-0.1.0/src/pyvand.egg-info/SOURCES.txt +29 -0
- pyvand-0.1.0/src/pyvand.egg-info/dependency_links.txt +1 -0
- pyvand-0.1.0/src/pyvand.egg-info/requires.txt +12 -0
- pyvand-0.1.0/src/pyvand.egg-info/top_level.txt +1 -0
pyvand-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyvand
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A world-class, modular framework for building production-ready AI chatbots.
|
|
5
|
+
Author-email: Pyvand Maintainers <hello@pyvand.ai>
|
|
6
|
+
Project-URL: Homepage, https://github.com/arxyzan/pyvand
|
|
7
|
+
Project-URL: Repository, https://github.com/arxyzan/pyvand
|
|
8
|
+
Project-URL: Documentation, https://github.com/arxyzan/pyvand#readme
|
|
9
|
+
Keywords: ai,chatbot,pydantic-ai,fastapi,framework
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: FastAPI
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: fastapi>=0.129.0
|
|
16
|
+
Requires-Dist: logfire[starlette]>=4.24.0
|
|
17
|
+
Requires-Dist: minio>=7.2.20
|
|
18
|
+
Requires-Dist: pydantic-ai-slim[openai]>=1.59.0
|
|
19
|
+
Requires-Dist: pyjwt>=2.11.0
|
|
20
|
+
Requires-Dist: pymongo>=4.16.0
|
|
21
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
22
|
+
Requires-Dist: python-multipart>=0.0.22
|
|
23
|
+
Requires-Dist: ruff>=0.15.1
|
|
24
|
+
Requires-Dist: sse-starlette>=3.2.0
|
|
25
|
+
Requires-Dist: starlette>=0.52.1
|
|
26
|
+
Requires-Dist: uvicorn>=0.40.0
|
|
27
|
+
|
|
28
|
+
# Pyvand Backend Framework (Python)
|
|
29
|
+
|
|
30
|
+
**Location:** `/packages/pyvand-python`
|
|
31
|
+
|
|
32
|
+
The core Python backend engine for the Pyvand Framework. This package provides a modular [FastAPI](https://fastapi.tiangolo.com/)-based framework for building AI chatbots with [Pydantic AI](https://ai.pydantic.dev/).
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **`PyvandApp` Factory**: Rapidly initialize a production-ready chatbot backend with standard API routes, security, and persistence.
|
|
37
|
+
- **Dependency Injection**: Reusable FastAPI dependencies for application-scoped resources like `Database` and `Storage` via `request.app.state`.
|
|
38
|
+
- **AI Agent Engine**: Orchestrate complex PydanticAI streaming agents with built-in instrumentation and event-driven architectures.
|
|
39
|
+
- **Atomic Balance Tracking**: Integrated per-user token and request balance monitoring and management.
|
|
40
|
+
- **Advanced Admin Management**: Secure API endpoints for user analytics, chat history auditing, and permission-based administration.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
As this is part of a monorepo, it is recommended to install it as an editable dependency for local development:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# In your app's pyproject.toml
|
|
48
|
+
[tool.uv.sources]
|
|
49
|
+
pyvand = { path = "../../../packages/pyvand-python", editable = true }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start (Minimal App)
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pyvand.app_factory import PyvandApp
|
|
56
|
+
|
|
57
|
+
# Initialize framework
|
|
58
|
+
app = PyvandApp()
|
|
59
|
+
|
|
60
|
+
# Your inner FastAPI application to serve via Uvicorn
|
|
61
|
+
# uv run uvicorn main:app.api --reload --port 8000
|
|
62
|
+
api = app.api
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Key Modules
|
|
66
|
+
|
|
67
|
+
- `pyvand.app_factory`: The main application orchestrator.
|
|
68
|
+
- `pyvand.api.dependencies`: Standard FastAPI dependency providers (`get_database`, `authenticate`, etc.).
|
|
69
|
+
- `pyvand.api.routes`: Built-in modular route groups for `auth`, `admin`, `chat`, and `users`.
|
|
70
|
+
- `pyvand.database`: MongoDB models and atomic transaction operations.
|
|
71
|
+
- `pyvand.storage`: MinIO/S3 integrated file storage module.
|
pyvand-0.1.0/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Pyvand Backend Framework (Python)
|
|
2
|
+
|
|
3
|
+
**Location:** `/packages/pyvand-python`
|
|
4
|
+
|
|
5
|
+
The core Python backend engine for the Pyvand Framework. This package provides a modular [FastAPI](https://fastapi.tiangolo.com/)-based framework for building AI chatbots with [Pydantic AI](https://ai.pydantic.dev/).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **`PyvandApp` Factory**: Rapidly initialize a production-ready chatbot backend with standard API routes, security, and persistence.
|
|
10
|
+
- **Dependency Injection**: Reusable FastAPI dependencies for application-scoped resources like `Database` and `Storage` via `request.app.state`.
|
|
11
|
+
- **AI Agent Engine**: Orchestrate complex PydanticAI streaming agents with built-in instrumentation and event-driven architectures.
|
|
12
|
+
- **Atomic Balance Tracking**: Integrated per-user token and request balance monitoring and management.
|
|
13
|
+
- **Advanced Admin Management**: Secure API endpoints for user analytics, chat history auditing, and permission-based administration.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
As this is part of a monorepo, it is recommended to install it as an editable dependency for local development:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# In your app's pyproject.toml
|
|
21
|
+
[tool.uv.sources]
|
|
22
|
+
pyvand = { path = "../../../packages/pyvand-python", editable = true }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start (Minimal App)
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from pyvand.app_factory import PyvandApp
|
|
29
|
+
|
|
30
|
+
# Initialize framework
|
|
31
|
+
app = PyvandApp()
|
|
32
|
+
|
|
33
|
+
# Your inner FastAPI application to serve via Uvicorn
|
|
34
|
+
# uv run uvicorn main:app.api --reload --port 8000
|
|
35
|
+
api = app.api
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Key Modules
|
|
39
|
+
|
|
40
|
+
- `pyvand.app_factory`: The main application orchestrator.
|
|
41
|
+
- `pyvand.api.dependencies`: Standard FastAPI dependency providers (`get_database`, `authenticate`, etc.).
|
|
42
|
+
- `pyvand.api.routes`: Built-in modular route groups for `auth`, `admin`, `chat`, and `users`.
|
|
43
|
+
- `pyvand.database`: MongoDB models and atomic transaction operations.
|
|
44
|
+
- `pyvand.storage`: MinIO/S3 integrated file storage module.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyvand"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A world-class, modular framework for building production-ready AI chatbots."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Pyvand Maintainers", email = "hello@pyvand.ai" },
|
|
9
|
+
]
|
|
10
|
+
keywords = ["ai", "chatbot", "pydantic-ai", "fastapi", "framework"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Framework :: FastAPI",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"fastapi>=0.129.0",
|
|
18
|
+
"logfire[starlette]>=4.24.0",
|
|
19
|
+
"minio>=7.2.20",
|
|
20
|
+
"pydantic-ai-slim[openai]>=1.59.0",
|
|
21
|
+
"pyjwt>=2.11.0",
|
|
22
|
+
"pymongo>=4.16.0",
|
|
23
|
+
"python-dotenv>=1.2.1",
|
|
24
|
+
"python-multipart>=0.0.22",
|
|
25
|
+
"ruff>=0.15.1",
|
|
26
|
+
"sse-starlette>=3.2.0",
|
|
27
|
+
"starlette>=0.52.1",
|
|
28
|
+
"uvicorn>=0.40.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/arxyzan/pyvand"
|
|
33
|
+
Repository = "https://github.com/arxyzan/pyvand"
|
|
34
|
+
Documentation = "https://github.com/arxyzan/pyvand#readme"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
lint.ignore = ["C901", "E501", "E741", "W605", "F403", "F405"]
|
|
39
|
+
lint.select = ["C", "E", "F", "I", "W"]
|
|
40
|
+
line-length = 120
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint.per-file-ignores]
|
|
43
|
+
"__init__.py" = ["E402", "F401", "F403", "F811", "I001"]
|
|
44
|
+
"*test*.py" = ["F401", "F841"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint.isort]
|
|
47
|
+
lines-after-imports = 2
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint.pydocstyle]
|
|
50
|
+
convention = "google"
|
pyvand-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic_ai import Agent, RunContext
|
|
4
|
+
|
|
5
|
+
from ..constants import OPENAI_HTTP_PROXY
|
|
6
|
+
from .deps import AgentDeps
|
|
7
|
+
from .utils import get_openai_model
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_AGENT_INSTRUCTIONS = """
|
|
11
|
+
You are a helpful AI assistant. Answer user questions clearly and concisely.
|
|
12
|
+
You can help with a wide range of topics including general knowledge, writing, analysis, and more.
|
|
13
|
+
|
|
14
|
+
Always be polite, accurate, and helpful. If you are unsure about something, let the user know.
|
|
15
|
+
""".strip()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_default_agent(
|
|
19
|
+
model_name: str = "gpt-5.1",
|
|
20
|
+
instructions: str | list[str] | None = None,
|
|
21
|
+
) -> Agent[AgentDeps[Any]]:
|
|
22
|
+
"""
|
|
23
|
+
Factory method to create a default agent with a customizable model and instructions.
|
|
24
|
+
"""
|
|
25
|
+
model = get_openai_model(model_name, proxy=OPENAI_HTTP_PROXY)
|
|
26
|
+
|
|
27
|
+
combined_instructions = DEFAULT_AGENT_INSTRUCTIONS
|
|
28
|
+
if instructions:
|
|
29
|
+
if isinstance(instructions, list):
|
|
30
|
+
combined_instructions += "\n" + "\n".join(instructions)
|
|
31
|
+
else:
|
|
32
|
+
combined_instructions += "\n" + instructions
|
|
33
|
+
|
|
34
|
+
agent = Agent[AgentDeps[Any]](
|
|
35
|
+
name="Agent",
|
|
36
|
+
instructions=combined_instructions,
|
|
37
|
+
model=model,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@agent.instructions
|
|
41
|
+
def add_user_info(ctx: RunContext[AgentDeps[Any]]):
|
|
42
|
+
info = ""
|
|
43
|
+
if ctx.deps.user is not None:
|
|
44
|
+
info += f"User's name and surname is '{ctx.deps.user.name} {ctx.deps.user.surname}'"
|
|
45
|
+
return info
|
|
46
|
+
|
|
47
|
+
return agent
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Default instance for legacy support or internal use
|
|
51
|
+
default_agent = get_default_agent()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, Generic, TypeVar
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
5
|
+
|
|
6
|
+
from ..database import Database, UserDocument
|
|
7
|
+
from ..storage import Storage
|
|
8
|
+
from .events import ThreadStreamEvent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
T_Context = TypeVar("T_Context")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentDeps[T_Context](BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Standard agent dependencies for the Pyvand framework.
|
|
17
|
+
Hides internal complexity (DB, Storage) from the developer.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
21
|
+
|
|
22
|
+
database: Database
|
|
23
|
+
storage: Storage
|
|
24
|
+
user: UserDocument
|
|
25
|
+
context: T_Context
|
|
26
|
+
|
|
27
|
+
# Internal event queue for streaming custom events
|
|
28
|
+
_event_queue: asyncio.Queue = PrivateAttr(default_factory=asyncio.Queue)
|
|
29
|
+
|
|
30
|
+
async def stream(self, event: ThreadStreamEvent):
|
|
31
|
+
await self._event_queue.put(event)
|
|
32
|
+
|
|
33
|
+
async def stream_events(self):
|
|
34
|
+
while True:
|
|
35
|
+
event = await self._event_queue.get()
|
|
36
|
+
if event is None:
|
|
37
|
+
break
|
|
38
|
+
yield event
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Annotated, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__all__ = ["ProgressUpdateEvent", "ThreadStreamEvent"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProgressUpdateEvent(BaseModel):
|
|
10
|
+
event_kind: Literal["progress_update"] = "progress_update"
|
|
11
|
+
tool_name: str
|
|
12
|
+
status: Literal["running", "completed", "error"]
|
|
13
|
+
message: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ThreadStreamEvent = Annotated[ProgressUpdateEvent, Field(discriminator="event_kind")]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A lightweight agent that generates concise chat titles (2-8 words)
|
|
3
|
+
using gpt-5-nano. Designed to run as a fire-and-forget background task.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from pydantic_ai import Agent
|
|
12
|
+
|
|
13
|
+
from ..constants import OPENAI_HTTP_PROXY
|
|
14
|
+
from .utils import get_openai_model
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..database import Database
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
TITLE_INSTRUCTIONS = (
|
|
23
|
+
"You are a title generator. Given a user question and an assistant response, "
|
|
24
|
+
"produce a short, descriptive title of 2 to 8 words that captures the topic. "
|
|
25
|
+
"Output ONLY the title text — no quotes, no explanation, no punctuation at the end."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_title_model = get_openai_model("gpt-5-nano", proxy=OPENAI_HTTP_PROXY)
|
|
29
|
+
|
|
30
|
+
title_agent = Agent(
|
|
31
|
+
name="TitleAgent",
|
|
32
|
+
instructions=TITLE_INSTRUCTIONS,
|
|
33
|
+
model=_title_model,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def generate_title(
|
|
38
|
+
question: str,
|
|
39
|
+
answer: str,
|
|
40
|
+
database: Database,
|
|
41
|
+
thread_id: str,
|
|
42
|
+
) -> str | None:
|
|
43
|
+
"""Generate a title for a thread and persist it. Returns the title or None on failure."""
|
|
44
|
+
try:
|
|
45
|
+
# Truncate inputs to keep the prompt small and fast
|
|
46
|
+
q = question[:500]
|
|
47
|
+
a = answer[:500]
|
|
48
|
+
prompt = f"User question:\n{q}\n\nAssistant response:\n{a}"
|
|
49
|
+
|
|
50
|
+
result = await title_agent.run(prompt)
|
|
51
|
+
title = result.output.strip()
|
|
52
|
+
|
|
53
|
+
if title:
|
|
54
|
+
# Enforce max length safety
|
|
55
|
+
if len(title) > 80:
|
|
56
|
+
title = title[:77] + "..."
|
|
57
|
+
database.update_thread_info(thread_id, title=title)
|
|
58
|
+
logger.info("Generated title for thread %s: %s", thread_id, title)
|
|
59
|
+
return title
|
|
60
|
+
except Exception:
|
|
61
|
+
logger.exception("Title generation failed for thread %s — keeping fallback title", thread_id)
|
|
62
|
+
return None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
__all__ = ["get_openai_model"]
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_openai_model(model_name: str, proxy: str | None = None):
|
|
5
|
+
"""
|
|
6
|
+
Create OpenAI model with optional proxy.
|
|
7
|
+
"""
|
|
8
|
+
import httpx
|
|
9
|
+
from pydantic_ai.models.openai import OpenAIChatModel
|
|
10
|
+
from pydantic_ai.providers.openai import OpenAIProvider
|
|
11
|
+
|
|
12
|
+
if proxy:
|
|
13
|
+
http_client = httpx.AsyncClient(
|
|
14
|
+
proxy=proxy,
|
|
15
|
+
timeout=30,
|
|
16
|
+
follow_redirects=True,
|
|
17
|
+
)
|
|
18
|
+
provider = OpenAIProvider(http_client=http_client)
|
|
19
|
+
else:
|
|
20
|
+
provider = OpenAIProvider()
|
|
21
|
+
|
|
22
|
+
openai_model = OpenAIChatModel(model_name=model_name, provider=provider)
|
|
23
|
+
|
|
24
|
+
return openai_model
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import jwt
|
|
2
|
+
from fastapi import Depends, HTTPException, Request
|
|
3
|
+
from fastapi.security import APIKeyHeader
|
|
4
|
+
|
|
5
|
+
from ..constants import (
|
|
6
|
+
ADMIN_JWT_SECRET_KEY,
|
|
7
|
+
BACKEND_API_KEY,
|
|
8
|
+
JWT_ALGORITHM,
|
|
9
|
+
JWT_SECRET_KEY,
|
|
10
|
+
)
|
|
11
|
+
from ..database import Database, UserDocument
|
|
12
|
+
from ..enums import AdminPermission
|
|
13
|
+
from ..storage import Storage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
api_key_header = APIKeyHeader(name="x-api-key", auto_error=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Singletons are now initialized by PyvandApp and attached to app.state
|
|
20
|
+
# Database and Storage instances will be retrieved from request.app.state
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_database(request: Request) -> Database:
|
|
24
|
+
return request.app.state.database
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_storage(request: Request) -> Storage:
|
|
28
|
+
return request.app.state.storage
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _verify_token(request: Request, secret: str, identity_key: str) -> str:
|
|
32
|
+
"""Helper to extract and verify a Bearer token from the Authorization header."""
|
|
33
|
+
auth_header = request.headers.get("Authorization")
|
|
34
|
+
if not auth_header:
|
|
35
|
+
raise HTTPException(status_code=401, detail="Authorization header missing")
|
|
36
|
+
|
|
37
|
+
parts = auth_header.split()
|
|
38
|
+
if len(parts) != 2 or parts[0].lower() != "bearer":
|
|
39
|
+
raise HTTPException(status_code=401, detail="Invalid authorization header format")
|
|
40
|
+
|
|
41
|
+
token = parts[1]
|
|
42
|
+
try:
|
|
43
|
+
payload = jwt.decode(token, secret, algorithms=[JWT_ALGORITHM])
|
|
44
|
+
identity = payload.get(identity_key)
|
|
45
|
+
if not identity:
|
|
46
|
+
raise HTTPException(status_code=401, detail="Invalid token payload")
|
|
47
|
+
return identity
|
|
48
|
+
except jwt.ExpiredSignatureError:
|
|
49
|
+
raise HTTPException(status_code=401, detail="Token has expired")
|
|
50
|
+
except jwt.InvalidTokenError:
|
|
51
|
+
raise HTTPException(status_code=401, detail="Invalid token")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def authenticate_with_bearer(request: Request) -> UserDocument:
|
|
55
|
+
"""Authenticate user by verifying JWT token from Authorization header."""
|
|
56
|
+
user_id = _verify_token(request, JWT_SECRET_KEY, "user_id")
|
|
57
|
+
database: Database = request.app.state.database
|
|
58
|
+
user_info = database.get_user(user_id)
|
|
59
|
+
if not user_info:
|
|
60
|
+
raise HTTPException(status_code=401, detail="User not found")
|
|
61
|
+
return user_info
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def authenticate_admin(request: Request) -> dict:
|
|
65
|
+
"""Authenticate admin by verifying session token."""
|
|
66
|
+
admin_id = _verify_token(request, ADMIN_JWT_SECRET_KEY, "admin_id")
|
|
67
|
+
database: Database = request.app.state.database
|
|
68
|
+
admin_info = database.get_admin(admin_id)
|
|
69
|
+
if not admin_info:
|
|
70
|
+
raise HTTPException(status_code=401, detail="Admin not found")
|
|
71
|
+
|
|
72
|
+
permissions = admin_info.permissions
|
|
73
|
+
if permissions and len(permissions) > 0 and not isinstance(permissions[0], str):
|
|
74
|
+
permissions = [p.value if hasattr(p, "value") else str(p) for p in permissions]
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"admin_id": admin_info.id,
|
|
78
|
+
"username": admin_info.username,
|
|
79
|
+
"name": admin_info.name,
|
|
80
|
+
"permissions": permissions,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def verify_api_key(api_key: str = Depends(api_key_header)) -> str:
|
|
85
|
+
"""Verify API key for server-to-server authentication."""
|
|
86
|
+
if api_key != BACKEND_API_KEY:
|
|
87
|
+
raise HTTPException(status_code=403, detail="Invalid API key")
|
|
88
|
+
return api_key
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def authenticate(
|
|
92
|
+
request: Request,
|
|
93
|
+
api_key: str | None = Depends(api_key_header),
|
|
94
|
+
) -> dict | None:
|
|
95
|
+
"""Authenticate with either admin token or API key."""
|
|
96
|
+
if api_key == BACKEND_API_KEY:
|
|
97
|
+
return None # API key authenticated
|
|
98
|
+
|
|
99
|
+
# Try admin authentication
|
|
100
|
+
return authenticate_admin(request)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def optional_authenticate(
|
|
104
|
+
request: Request,
|
|
105
|
+
api_key: str | None = Depends(api_key_header),
|
|
106
|
+
) -> dict | None:
|
|
107
|
+
"""Try to authenticate, but return None if no credentials provided."""
|
|
108
|
+
try:
|
|
109
|
+
return authenticate(request, api_key)
|
|
110
|
+
except HTTPException:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def check_permission(admin: dict | None, permission: AdminPermission):
|
|
115
|
+
"""Check if admin has the required permission."""
|
|
116
|
+
if admin is None:
|
|
117
|
+
# This happens when authenticated via API key
|
|
118
|
+
return
|
|
119
|
+
if permission.value not in admin.get("permissions", []):
|
|
120
|
+
raise HTTPException(
|
|
121
|
+
status_code=403,
|
|
122
|
+
detail=f"Permission denied: {permission.value} required",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def require_permission(permission: AdminPermission):
|
|
127
|
+
"""Dependency factory to require a specific admin permission."""
|
|
128
|
+
|
|
129
|
+
def _permission_dependency(admin: dict | None = Depends(authenticate)):
|
|
130
|
+
check_permission(admin, permission)
|
|
131
|
+
return admin
|
|
132
|
+
|
|
133
|
+
return _permission_dependency
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from ..enums import AdminPermission
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ------------------------------ #
|
|
7
|
+
# -------- User Inputs --------- #
|
|
8
|
+
# ------------------------------ #
|
|
9
|
+
class RegisterUserInput(BaseModel):
|
|
10
|
+
phone: str
|
|
11
|
+
password: str
|
|
12
|
+
name: str
|
|
13
|
+
surname: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoginInput(BaseModel):
|
|
17
|
+
phone: str
|
|
18
|
+
password: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MessageFeedbackInput(BaseModel):
|
|
22
|
+
thread_id: str
|
|
23
|
+
message_id: str
|
|
24
|
+
sentiment: str # 'like' or 'dislike'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ThreadUpdateInput(BaseModel):
|
|
28
|
+
title: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ------------------------------ #
|
|
32
|
+
# -------- Admin Inputs -------- #
|
|
33
|
+
# ------------------------------ #
|
|
34
|
+
class AdminRegisterInput(BaseModel):
|
|
35
|
+
username: str
|
|
36
|
+
password: str
|
|
37
|
+
name: str
|
|
38
|
+
permissions: list[AdminPermission]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AdminUpdateInput(BaseModel):
|
|
42
|
+
name: str | None = None
|
|
43
|
+
password: str | None = None
|
|
44
|
+
permissions: list[AdminPermission] | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AdminLoginInput(BaseModel):
|
|
48
|
+
username: str
|
|
49
|
+
password: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class UserCreateInput(BaseModel):
|
|
53
|
+
phone: str
|
|
54
|
+
name: str
|
|
55
|
+
surname: str
|
|
56
|
+
balance: dict | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class UserUpdateInput(BaseModel):
|
|
60
|
+
phone: str | None = None
|
|
61
|
+
name: str | None = None
|
|
62
|
+
surname: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class UserBalanceUpdateInput(BaseModel):
|
|
66
|
+
tokens: int
|
|
67
|
+
requests: int
|