py-aidol 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.
- py_aidol-0.1.0/PKG-INFO +114 -0
- py_aidol-0.1.0/README.md +85 -0
- py_aidol-0.1.0/aidol/__init__.py +7 -0
- py_aidol-0.1.0/aidol/api/__init__.py +13 -0
- py_aidol-0.1.0/aidol/api/aidol.py +189 -0
- py_aidol-0.1.0/aidol/api/companion.py +168 -0
- py_aidol-0.1.0/aidol/factories.py +24 -0
- py_aidol-0.1.0/aidol/models/__init__.py +8 -0
- py_aidol-0.1.0/aidol/models/aidol.py +24 -0
- py_aidol-0.1.0/aidol/models/companion.py +27 -0
- py_aidol-0.1.0/aidol/protocols.py +100 -0
- py_aidol-0.1.0/aidol/py.typed +0 -0
- py_aidol-0.1.0/aidol/repositories/__init__.py +11 -0
- py_aidol-0.1.0/aidol/repositories/aidol.py +57 -0
- py_aidol-0.1.0/aidol/repositories/companion.py +57 -0
- py_aidol-0.1.0/aidol/schemas/__init__.py +37 -0
- py_aidol-0.1.0/aidol/schemas/aidol.py +119 -0
- py_aidol-0.1.0/aidol/schemas/companion.py +93 -0
- py_aidol-0.1.0/aidol/services/__init__.py +9 -0
- py_aidol-0.1.0/aidol/services/image_generation_service.py +145 -0
- py_aidol-0.1.0/pyproject.toml +69 -0
py_aidol-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-aidol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Create and chat with your own AI idol group
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: kpop,idol,aidol,ai-companion,chatbot,image-generation
|
|
7
|
+
Author: AIoIA, Inc.
|
|
8
|
+
Author-email: devops@aioia.ai
|
|
9
|
+
Requires-Python: >=3.10,<3.13
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Dist: aioia-core (>=2.2.0,<3.0.0)
|
|
16
|
+
Requires-Dist: fastapi (>=0.115.12,<0.116.0)
|
|
17
|
+
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
18
|
+
Requires-Dist: openai (>=1.0.0)
|
|
19
|
+
Requires-Dist: pillow (>=10.0.0,<11.0.0)
|
|
20
|
+
Requires-Dist: psycopg2-binary (>=2.9.9,<3.0.0)
|
|
21
|
+
Requires-Dist: pydantic-settings (>=2.1.0,<3.0.0)
|
|
22
|
+
Requires-Dist: pydantic[email] (>=2.5.3,<3.0.0)
|
|
23
|
+
Requires-Dist: pyhumps (>=3.8.0,<4.0.0)
|
|
24
|
+
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
25
|
+
Requires-Dist: sqlalchemy (>=2.0.25,<3.0.0)
|
|
26
|
+
Requires-Dist: sqlalchemy-mixins (>=2.0.5,<3.0.0)
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# AIdol Backend
|
|
30
|
+
|
|
31
|
+
AI 아이돌 그룹 생성 및 채팅 Python 패키지
|
|
32
|
+
|
|
33
|
+
## 주요 기능
|
|
34
|
+
|
|
35
|
+
- AI 아이돌 그룹/멤버 CRUD
|
|
36
|
+
- DALL-E 3 이미지 생성 (엠블럼, 프로필)
|
|
37
|
+
- 텍스트 채팅 (페르소나 기반 응답)
|
|
38
|
+
- Buppy 통합 Adapter 패턴
|
|
39
|
+
|
|
40
|
+
## 설치
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd backend
|
|
44
|
+
poetry install
|
|
45
|
+
poetry run uvicorn main:app --reload
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
API 문서:
|
|
49
|
+
- Swagger UI: http://localhost:8000/docs
|
|
50
|
+
- ReDoc: http://localhost:8000/redoc
|
|
51
|
+
|
|
52
|
+
## 사용법
|
|
53
|
+
|
|
54
|
+
### FastAPI 통합
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from aidol.api.aidol import AIdolRouter
|
|
58
|
+
from aidol.api.companion import CompanionRouter
|
|
59
|
+
from aidol.factories import AIdolRepositoryFactory, CompanionRepositoryFactory
|
|
60
|
+
|
|
61
|
+
# AIdol 라우터
|
|
62
|
+
aidol_router = AIdolRouter(
|
|
63
|
+
repository_factory=AIdolRepositoryFactory(),
|
|
64
|
+
openai_settings=openai_settings,
|
|
65
|
+
image_storage=image_storage,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Companion 라우터
|
|
69
|
+
companion_router = CompanionRouter(
|
|
70
|
+
repository_factory=CompanionRepositoryFactory(),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
app.include_router(aidol_router.router, prefix="/api/aidol")
|
|
74
|
+
app.include_router(companion_router.router, prefix="/api/aidol")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 개발
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
poetry install
|
|
81
|
+
make lint
|
|
82
|
+
make type-check
|
|
83
|
+
make unit-test
|
|
84
|
+
make format
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 환경 변수
|
|
88
|
+
|
|
89
|
+
### 필수 (이미지 생성 시)
|
|
90
|
+
|
|
91
|
+
| 변수 | 설명 |
|
|
92
|
+
|------|------|
|
|
93
|
+
| `OPENAI_API_KEY` | OpenAI API 키 |
|
|
94
|
+
|
|
95
|
+
### 선택
|
|
96
|
+
|
|
97
|
+
| 변수 | 기본값 | 설명 |
|
|
98
|
+
|------|--------|------|
|
|
99
|
+
| `AIDOL_OPENAI_MODEL` | `gpt-4o-mini` | 채팅 응답 LLM 모델 |
|
|
100
|
+
|
|
101
|
+
> **참고**: 데이터베이스, 모델 등 추가 설정은 기본값으로 로컬 개발 가능합니다.
|
|
102
|
+
> 변경이 필요한 경우 `aidol/` 내 Settings 클래스를 참고하세요.
|
|
103
|
+
|
|
104
|
+
## 의존성
|
|
105
|
+
|
|
106
|
+
- aioia-core (공통 인프라)
|
|
107
|
+
- FastAPI, SQLAlchemy, Pydantic
|
|
108
|
+
- OpenAI (이미지 생성, 채팅)
|
|
109
|
+
- Pillow (이미지 처리)
|
|
110
|
+
|
|
111
|
+
## 라이선스
|
|
112
|
+
|
|
113
|
+
Apache 2.0
|
|
114
|
+
|
py_aidol-0.1.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# AIdol Backend
|
|
2
|
+
|
|
3
|
+
AI 아이돌 그룹 생성 및 채팅 Python 패키지
|
|
4
|
+
|
|
5
|
+
## 주요 기능
|
|
6
|
+
|
|
7
|
+
- AI 아이돌 그룹/멤버 CRUD
|
|
8
|
+
- DALL-E 3 이미지 생성 (엠블럼, 프로필)
|
|
9
|
+
- 텍스트 채팅 (페르소나 기반 응답)
|
|
10
|
+
- Buppy 통합 Adapter 패턴
|
|
11
|
+
|
|
12
|
+
## 설치
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd backend
|
|
16
|
+
poetry install
|
|
17
|
+
poetry run uvicorn main:app --reload
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
API 문서:
|
|
21
|
+
- Swagger UI: http://localhost:8000/docs
|
|
22
|
+
- ReDoc: http://localhost:8000/redoc
|
|
23
|
+
|
|
24
|
+
## 사용법
|
|
25
|
+
|
|
26
|
+
### FastAPI 통합
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from aidol.api.aidol import AIdolRouter
|
|
30
|
+
from aidol.api.companion import CompanionRouter
|
|
31
|
+
from aidol.factories import AIdolRepositoryFactory, CompanionRepositoryFactory
|
|
32
|
+
|
|
33
|
+
# AIdol 라우터
|
|
34
|
+
aidol_router = AIdolRouter(
|
|
35
|
+
repository_factory=AIdolRepositoryFactory(),
|
|
36
|
+
openai_settings=openai_settings,
|
|
37
|
+
image_storage=image_storage,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Companion 라우터
|
|
41
|
+
companion_router = CompanionRouter(
|
|
42
|
+
repository_factory=CompanionRepositoryFactory(),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
app.include_router(aidol_router.router, prefix="/api/aidol")
|
|
46
|
+
app.include_router(companion_router.router, prefix="/api/aidol")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 개발
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
poetry install
|
|
53
|
+
make lint
|
|
54
|
+
make type-check
|
|
55
|
+
make unit-test
|
|
56
|
+
make format
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 환경 변수
|
|
60
|
+
|
|
61
|
+
### 필수 (이미지 생성 시)
|
|
62
|
+
|
|
63
|
+
| 변수 | 설명 |
|
|
64
|
+
|------|------|
|
|
65
|
+
| `OPENAI_API_KEY` | OpenAI API 키 |
|
|
66
|
+
|
|
67
|
+
### 선택
|
|
68
|
+
|
|
69
|
+
| 변수 | 기본값 | 설명 |
|
|
70
|
+
|------|--------|------|
|
|
71
|
+
| `AIDOL_OPENAI_MODEL` | `gpt-4o-mini` | 채팅 응답 LLM 모델 |
|
|
72
|
+
|
|
73
|
+
> **참고**: 데이터베이스, 모델 등 추가 설정은 기본값으로 로컬 개발 가능합니다.
|
|
74
|
+
> 변경이 필요한 경우 `aidol/` 내 Settings 클래스를 참고하세요.
|
|
75
|
+
|
|
76
|
+
## 의존성
|
|
77
|
+
|
|
78
|
+
- aioia-core (공통 인프라)
|
|
79
|
+
- FastAPI, SQLAlchemy, Pydantic
|
|
80
|
+
- OpenAI (이미지 생성, 채팅)
|
|
81
|
+
- Pillow (이미지 처리)
|
|
82
|
+
|
|
83
|
+
## 라이선스
|
|
84
|
+
|
|
85
|
+
Apache 2.0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol API routers
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from aidol.api.aidol import AIdolRouter, create_aidol_router
|
|
6
|
+
from aidol.api.companion import CompanionRouter, create_companion_router
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AIdolRouter",
|
|
10
|
+
"CompanionRouter",
|
|
11
|
+
"create_aidol_router",
|
|
12
|
+
"create_companion_router",
|
|
13
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol API router
|
|
3
|
+
|
|
4
|
+
Public endpoints for AIdol group creation and retrieval.
|
|
5
|
+
Public access pattern: no authentication required.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from aioia_core.auth import UserInfoProvider
|
|
9
|
+
from aioia_core.errors import ErrorResponse
|
|
10
|
+
from aioia_core.fastapi import BaseCrudRouter
|
|
11
|
+
from aioia_core.settings import JWTSettings, OpenAIAPISettings
|
|
12
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from sqlalchemy.orm import sessionmaker
|
|
15
|
+
|
|
16
|
+
from aidol.protocols import (
|
|
17
|
+
AIdolRepositoryFactoryProtocol,
|
|
18
|
+
AIdolRepositoryProtocol,
|
|
19
|
+
ImageStorageProtocol,
|
|
20
|
+
)
|
|
21
|
+
from aidol.schemas import (
|
|
22
|
+
AIdol,
|
|
23
|
+
AIdolCreate,
|
|
24
|
+
AIdolPublic,
|
|
25
|
+
AIdolUpdate,
|
|
26
|
+
ImageGenerationData,
|
|
27
|
+
ImageGenerationRequest,
|
|
28
|
+
ImageGenerationResponse,
|
|
29
|
+
)
|
|
30
|
+
from aidol.services import ImageGenerationService
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AIdolSingleItemResponse(BaseModel):
|
|
34
|
+
"""Single item response for AIdol (public)."""
|
|
35
|
+
|
|
36
|
+
data: AIdolPublic
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AIdolRouter(
|
|
40
|
+
BaseCrudRouter[AIdol, AIdolCreate, AIdolUpdate, AIdolRepositoryProtocol]
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
AIdol router with public endpoints.
|
|
44
|
+
|
|
45
|
+
Public CRUD pattern: no authentication required.
|
|
46
|
+
Returns AIdolPublic (excludes claim_token) for all responses.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
openai_settings: OpenAIAPISettings,
|
|
52
|
+
image_storage: ImageStorageProtocol,
|
|
53
|
+
**kwargs,
|
|
54
|
+
):
|
|
55
|
+
super().__init__(**kwargs)
|
|
56
|
+
self.openai_settings = openai_settings
|
|
57
|
+
self.image_storage = image_storage
|
|
58
|
+
|
|
59
|
+
def _register_routes(self) -> None:
|
|
60
|
+
"""Register routes (public CRUD + image generation)"""
|
|
61
|
+
self._register_image_generation_route()
|
|
62
|
+
self._register_public_create_route()
|
|
63
|
+
self._register_public_get_route()
|
|
64
|
+
|
|
65
|
+
def _register_public_create_route(self) -> None:
|
|
66
|
+
"""POST /{resource_name} - Create an AIdol group (public)"""
|
|
67
|
+
|
|
68
|
+
@self.router.post(
|
|
69
|
+
f"/{self.resource_name}",
|
|
70
|
+
response_model=AIdolSingleItemResponse,
|
|
71
|
+
status_code=status.HTTP_201_CREATED,
|
|
72
|
+
summary="Create AIdol group",
|
|
73
|
+
description="Create a new AIdol group (public endpoint)",
|
|
74
|
+
)
|
|
75
|
+
async def create_aidol(
|
|
76
|
+
request: AIdolCreate,
|
|
77
|
+
repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
|
|
78
|
+
):
|
|
79
|
+
"""Create a new AIdol group."""
|
|
80
|
+
created = repository.create(request)
|
|
81
|
+
# Convert to Public schema (exclude claim_token)
|
|
82
|
+
public_aidol = AIdolPublic(**created.model_dump())
|
|
83
|
+
return AIdolSingleItemResponse(data=public_aidol)
|
|
84
|
+
|
|
85
|
+
def _register_public_get_route(self) -> None:
|
|
86
|
+
"""GET /{resource_name}/{id} - Get an AIdol group (public)"""
|
|
87
|
+
|
|
88
|
+
@self.router.get(
|
|
89
|
+
f"/{self.resource_name}/{{item_id}}",
|
|
90
|
+
response_model=AIdolSingleItemResponse,
|
|
91
|
+
status_code=status.HTTP_200_OK,
|
|
92
|
+
summary="Get AIdol group",
|
|
93
|
+
description="Get AIdol group by ID (public endpoint)",
|
|
94
|
+
responses={
|
|
95
|
+
404: {"model": ErrorResponse, "description": "AIdol group not found"},
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
async def get_aidol(
|
|
99
|
+
item_id: str,
|
|
100
|
+
repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
|
|
101
|
+
):
|
|
102
|
+
"""Get AIdol group by ID."""
|
|
103
|
+
aidol = self._get_item_or_404(repository, item_id)
|
|
104
|
+
# Convert to Public schema (exclude claim_token)
|
|
105
|
+
public_aidol = AIdolPublic(**aidol.model_dump())
|
|
106
|
+
return AIdolSingleItemResponse(data=public_aidol)
|
|
107
|
+
|
|
108
|
+
def _register_image_generation_route(self) -> None:
|
|
109
|
+
"""POST /{resource_name}/images - Generate image for AIdol or Companion"""
|
|
110
|
+
|
|
111
|
+
@self.router.post(
|
|
112
|
+
f"/{self.resource_name}/images",
|
|
113
|
+
response_model=ImageGenerationResponse,
|
|
114
|
+
status_code=status.HTTP_201_CREATED,
|
|
115
|
+
summary="Generate image",
|
|
116
|
+
description="Generate image for AIdol emblem or Companion profile",
|
|
117
|
+
responses={
|
|
118
|
+
500: {"model": ErrorResponse, "description": "Image generation failed"},
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
async def generate_image(request: ImageGenerationRequest):
|
|
122
|
+
"""Generate image from prompt."""
|
|
123
|
+
# Generate and download image (TTS pattern: service returns data)
|
|
124
|
+
service = ImageGenerationService(self.openai_settings)
|
|
125
|
+
image = service.generate_and_download_image(
|
|
126
|
+
prompt=request.prompt,
|
|
127
|
+
size="1024x1024",
|
|
128
|
+
quality="standard",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if image is None:
|
|
132
|
+
raise HTTPException(
|
|
133
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
134
|
+
detail="Image generation failed",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Upload to permanent storage (TTS pattern: API layer orchestrates)
|
|
138
|
+
image_url = self.image_storage.upload_image(image)
|
|
139
|
+
|
|
140
|
+
return ImageGenerationResponse(
|
|
141
|
+
data=ImageGenerationData(
|
|
142
|
+
image_url=image_url,
|
|
143
|
+
width=1024,
|
|
144
|
+
height=1024,
|
|
145
|
+
format="png",
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def create_aidol_router(
|
|
151
|
+
openai_settings: OpenAIAPISettings,
|
|
152
|
+
db_session_factory: sessionmaker,
|
|
153
|
+
repository_factory: AIdolRepositoryFactoryProtocol,
|
|
154
|
+
image_storage: ImageStorageProtocol,
|
|
155
|
+
jwt_settings: JWTSettings | None = None,
|
|
156
|
+
user_info_provider: UserInfoProvider | None = None,
|
|
157
|
+
resource_name: str = "aidols",
|
|
158
|
+
tags: list[str] | None = None,
|
|
159
|
+
) -> APIRouter:
|
|
160
|
+
"""
|
|
161
|
+
Create AIdol router with dependency injection.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
openai_settings: OpenAI API settings for image generation
|
|
165
|
+
db_session_factory: Database session factory
|
|
166
|
+
repository_factory: Factory implementing AIdolRepositoryFactoryProtocol
|
|
167
|
+
image_storage: Image storage for permanent URLs
|
|
168
|
+
jwt_settings: Optional JWT settings for authentication
|
|
169
|
+
user_info_provider: Optional user info provider
|
|
170
|
+
resource_name: Resource name for routes (default: "aidols")
|
|
171
|
+
tags: Optional OpenAPI tags
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
FastAPI APIRouter instance
|
|
175
|
+
"""
|
|
176
|
+
router = AIdolRouter(
|
|
177
|
+
openai_settings=openai_settings,
|
|
178
|
+
image_storage=image_storage,
|
|
179
|
+
model_class=AIdol,
|
|
180
|
+
create_schema=AIdolCreate,
|
|
181
|
+
update_schema=AIdolUpdate,
|
|
182
|
+
db_session_factory=db_session_factory,
|
|
183
|
+
repository_factory=repository_factory,
|
|
184
|
+
user_info_provider=user_info_provider,
|
|
185
|
+
jwt_secret_key=jwt_settings.secret_key if jwt_settings else None,
|
|
186
|
+
resource_name=resource_name,
|
|
187
|
+
tags=tags or ["AIdol"],
|
|
188
|
+
)
|
|
189
|
+
return router.get_router()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Companion API router
|
|
3
|
+
|
|
4
|
+
Public endpoints for Companion creation and retrieval.
|
|
5
|
+
Public access pattern: no authentication required.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from aioia_core.auth import UserInfoProvider
|
|
9
|
+
from aioia_core.errors import ErrorResponse
|
|
10
|
+
from aioia_core.fastapi import BaseCrudRouter
|
|
11
|
+
from aioia_core.settings import JWTSettings
|
|
12
|
+
from fastapi import APIRouter, Depends, Query, status
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from sqlalchemy.orm import sessionmaker
|
|
15
|
+
|
|
16
|
+
from aidol.protocols import (
|
|
17
|
+
CompanionRepositoryFactoryProtocol,
|
|
18
|
+
CompanionRepositoryProtocol,
|
|
19
|
+
)
|
|
20
|
+
from aidol.schemas import Companion, CompanionCreate, CompanionPublic, CompanionUpdate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CompanionSingleItemResponse(BaseModel):
|
|
24
|
+
"""Single item response for Companion (public)."""
|
|
25
|
+
|
|
26
|
+
data: CompanionPublic
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CompanionPaginatedResponse(BaseModel):
|
|
30
|
+
"""Paginated response for Companion (public)."""
|
|
31
|
+
|
|
32
|
+
data: list[CompanionPublic]
|
|
33
|
+
total: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CompanionRouter(
|
|
37
|
+
BaseCrudRouter[
|
|
38
|
+
Companion, CompanionCreate, CompanionUpdate, CompanionRepositoryProtocol
|
|
39
|
+
]
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Companion router with public endpoints.
|
|
43
|
+
|
|
44
|
+
Public CRUD pattern: no authentication required.
|
|
45
|
+
Returns CompanionPublic (excludes system_prompt) for all responses.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def _register_routes(self) -> None:
|
|
49
|
+
"""Register routes (public CRUD)"""
|
|
50
|
+
self._register_public_list_route()
|
|
51
|
+
self._register_public_create_route()
|
|
52
|
+
self._register_public_get_route()
|
|
53
|
+
|
|
54
|
+
def _register_public_list_route(self) -> None:
|
|
55
|
+
"""GET /{resource_name} - List Companions (public)"""
|
|
56
|
+
|
|
57
|
+
@self.router.get(
|
|
58
|
+
f"/{self.resource_name}",
|
|
59
|
+
response_model=CompanionPaginatedResponse,
|
|
60
|
+
status_code=status.HTTP_200_OK,
|
|
61
|
+
summary="List Companions",
|
|
62
|
+
description="List all Companions with optional filtering (public endpoint)",
|
|
63
|
+
)
|
|
64
|
+
async def list_companions(
|
|
65
|
+
current: int = Query(1, ge=1, description="Current page number"),
|
|
66
|
+
page_size: int = Query(10, ge=1, le=100, description="Items per page"),
|
|
67
|
+
sort_param: str | None = Query(
|
|
68
|
+
None,
|
|
69
|
+
alias="sort",
|
|
70
|
+
description='Sorting criteria in JSON format. Example: [["createdAt","desc"]]',
|
|
71
|
+
),
|
|
72
|
+
filters_param: str | None = Query(
|
|
73
|
+
None,
|
|
74
|
+
alias="filters",
|
|
75
|
+
description="Filter conditions (JSON format)",
|
|
76
|
+
),
|
|
77
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
78
|
+
):
|
|
79
|
+
"""List Companions with pagination, sorting, and filtering."""
|
|
80
|
+
sort_list, filter_list = self._parse_query_params(sort_param, filters_param)
|
|
81
|
+
items, total = repository.get_all(
|
|
82
|
+
current=current,
|
|
83
|
+
page_size=page_size,
|
|
84
|
+
sort=sort_list,
|
|
85
|
+
filters=filter_list,
|
|
86
|
+
)
|
|
87
|
+
# Convert to Public schema (exclude system_prompt)
|
|
88
|
+
public_items = [CompanionPublic(**c.model_dump()) for c in items]
|
|
89
|
+
return CompanionPaginatedResponse(data=public_items, total=total)
|
|
90
|
+
|
|
91
|
+
def _register_public_create_route(self) -> None:
|
|
92
|
+
"""POST /{resource_name} - Create a Companion (public)"""
|
|
93
|
+
|
|
94
|
+
@self.router.post(
|
|
95
|
+
f"/{self.resource_name}",
|
|
96
|
+
response_model=CompanionSingleItemResponse,
|
|
97
|
+
status_code=status.HTTP_201_CREATED,
|
|
98
|
+
summary="Create Companion",
|
|
99
|
+
description="Create a new Companion (public endpoint)",
|
|
100
|
+
)
|
|
101
|
+
async def create_companion(
|
|
102
|
+
request: CompanionCreate,
|
|
103
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
104
|
+
):
|
|
105
|
+
"""Create a new Companion."""
|
|
106
|
+
created = repository.create(request)
|
|
107
|
+
# Convert to Public schema (exclude system_prompt)
|
|
108
|
+
public_companion = CompanionPublic(**created.model_dump())
|
|
109
|
+
return CompanionSingleItemResponse(data=public_companion)
|
|
110
|
+
|
|
111
|
+
def _register_public_get_route(self) -> None:
|
|
112
|
+
"""GET /{resource_name}/{id} - Get a Companion (public)"""
|
|
113
|
+
|
|
114
|
+
@self.router.get(
|
|
115
|
+
f"/{self.resource_name}/{{item_id}}",
|
|
116
|
+
response_model=CompanionSingleItemResponse,
|
|
117
|
+
status_code=status.HTTP_200_OK,
|
|
118
|
+
summary="Get Companion",
|
|
119
|
+
description="Get Companion by ID (public endpoint)",
|
|
120
|
+
responses={
|
|
121
|
+
404: {"model": ErrorResponse, "description": "Companion not found"},
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
async def get_companion(
|
|
125
|
+
item_id: str,
|
|
126
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
127
|
+
):
|
|
128
|
+
"""Get Companion by ID."""
|
|
129
|
+
companion = self._get_item_or_404(repository, item_id)
|
|
130
|
+
# Convert to Public schema (exclude system_prompt)
|
|
131
|
+
public_companion = CompanionPublic(**companion.model_dump())
|
|
132
|
+
return CompanionSingleItemResponse(data=public_companion)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def create_companion_router(
|
|
136
|
+
db_session_factory: sessionmaker,
|
|
137
|
+
repository_factory: CompanionRepositoryFactoryProtocol,
|
|
138
|
+
jwt_settings: JWTSettings | None = None,
|
|
139
|
+
user_info_provider: UserInfoProvider | None = None,
|
|
140
|
+
resource_name: str = "companions",
|
|
141
|
+
tags: list[str] | None = None,
|
|
142
|
+
) -> APIRouter:
|
|
143
|
+
"""
|
|
144
|
+
Create Companion router with dependency injection.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
db_session_factory: Database session factory
|
|
148
|
+
repository_factory: Factory implementing CompanionRepositoryFactoryProtocol
|
|
149
|
+
jwt_settings: Optional JWT settings for authentication
|
|
150
|
+
user_info_provider: Optional user info provider
|
|
151
|
+
resource_name: Resource name for routes (default: "companions")
|
|
152
|
+
tags: Optional OpenAPI tags
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
FastAPI APIRouter instance
|
|
156
|
+
"""
|
|
157
|
+
router = CompanionRouter(
|
|
158
|
+
model_class=Companion,
|
|
159
|
+
create_schema=CompanionCreate,
|
|
160
|
+
update_schema=CompanionUpdate,
|
|
161
|
+
db_session_factory=db_session_factory,
|
|
162
|
+
repository_factory=repository_factory,
|
|
163
|
+
user_info_provider=user_info_provider,
|
|
164
|
+
jwt_secret_key=jwt_settings.secret_key if jwt_settings else None,
|
|
165
|
+
resource_name=resource_name,
|
|
166
|
+
tags=tags or ["Companion"],
|
|
167
|
+
)
|
|
168
|
+
return router.get_router()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol repository factories
|
|
3
|
+
|
|
4
|
+
Uses BaseRepositoryFactory for BaseCrudRouter compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from aioia_core.factories import BaseRepositoryFactory
|
|
8
|
+
|
|
9
|
+
from aidol.repositories.aidol import AIdolRepository
|
|
10
|
+
from aidol.repositories.companion import CompanionRepository
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AIdolRepositoryFactory(BaseRepositoryFactory[AIdolRepository]):
|
|
14
|
+
"""Factory for creating AIdol repositories."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__(repository_class=AIdolRepository)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CompanionRepositoryFactory(BaseRepositoryFactory[CompanionRepository]):
|
|
21
|
+
"""Factory for creating Companion repositories."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__(repository_class=CompanionRepository)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol database model
|
|
3
|
+
|
|
4
|
+
Uses aioia_core.models.BaseModel which provides:
|
|
5
|
+
- id: Mapped[str] (primary key, UUID default)
|
|
6
|
+
- created_at: Mapped[datetime]
|
|
7
|
+
- updated_at: Mapped[datetime]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from aioia_core.models import BaseModel
|
|
11
|
+
from sqlalchemy import String
|
|
12
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DBAIdol(BaseModel):
|
|
16
|
+
"""AIdol (group) database model"""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "aidols"
|
|
19
|
+
|
|
20
|
+
# id, created_at, updated_at inherited from BaseModel
|
|
21
|
+
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
22
|
+
concept: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
23
|
+
profile_image_url: Mapped[str] = mapped_column(String, nullable=False)
|
|
24
|
+
claim_token: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Companion database model
|
|
3
|
+
|
|
4
|
+
Uses aioia_core.models.BaseModel which provides:
|
|
5
|
+
- id: Mapped[str] (primary key, UUID default)
|
|
6
|
+
- created_at: Mapped[datetime]
|
|
7
|
+
- updated_at: Mapped[datetime]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from aioia_core.models import BaseModel
|
|
11
|
+
from sqlalchemy import ForeignKey, Index, String, Text
|
|
12
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DBCompanion(BaseModel):
|
|
16
|
+
"""Companion (member) database model"""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "companions"
|
|
19
|
+
|
|
20
|
+
# id, created_at, updated_at inherited from BaseModel
|
|
21
|
+
aidol_id: Mapped[str | None] = mapped_column(ForeignKey("aidols.id"), nullable=True)
|
|
22
|
+
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
23
|
+
biography: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
24
|
+
profile_picture_url: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
25
|
+
system_prompt: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
26
|
+
|
|
27
|
+
__table_args__ = (Index("ix_companions_aidol_id", "aidol_id"),)
|