py-aidol 0.2.0__tar.gz → 0.3.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.2.0 → py_aidol-0.3.0}/PKG-INFO +2 -1
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/api/aidol.py +67 -73
- py_aidol-0.3.0/aidol/api/common.py +71 -0
- py_aidol-0.3.0/aidol/api/companion.py +285 -0
- py_aidol-0.3.0/aidol/api/lead.py +123 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/factories.py +8 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/protocols.py +26 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/repositories/aidol_lead.py +3 -6
- py_aidol-0.3.0/aidol/services/companion_service.py +96 -0
- py_aidol-0.3.0/aidol/services/image_generation_service.py +100 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/pyproject.toml +2 -1
- py_aidol-0.2.0/aidol/api/companion.py +0 -168
- py_aidol-0.2.0/aidol/services/image_generation_service.py +0 -145
- {py_aidol-0.2.0 → py_aidol-0.3.0}/README.md +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/__init__.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/api/__init__.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/models/__init__.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/models/aidol.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/models/aidol_lead.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/models/companion.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/py.typed +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/repositories/__init__.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/repositories/aidol.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/repositories/companion.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/schemas/__init__.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/schemas/aidol.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/schemas/aidol_lead.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/schemas/companion.py +0 -0
- {py_aidol-0.2.0 → py_aidol-0.3.0}/aidol/services/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-aidol
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Create and chat with your own AI idol group
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: kpop,idol,aidol,ai-companion,chatbot,image-generation
|
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Requires-Dist: aioia-core (>=2.2.0,<3.0.0)
|
|
16
16
|
Requires-Dist: fastapi (>=0.115.12,<0.116.0)
|
|
17
|
+
Requires-Dist: google-genai (>=1.60.0,<2.0.0)
|
|
17
18
|
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
18
19
|
Requires-Dist: openai (>=1.0.0)
|
|
19
20
|
Requires-Dist: pillow (>=10.0.0,<11.0.0)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pylint: disable=duplicate-code
|
|
1
2
|
"""
|
|
2
3
|
AIdol API router
|
|
3
4
|
|
|
@@ -8,32 +9,24 @@ Public access pattern: no authentication required.
|
|
|
8
9
|
from aioia_core.auth import UserInfoProvider
|
|
9
10
|
from aioia_core.errors import ErrorResponse
|
|
10
11
|
from aioia_core.fastapi import BaseCrudRouter
|
|
11
|
-
from aioia_core.settings import JWTSettings
|
|
12
|
-
from fastapi import APIRouter, Depends, HTTPException, status
|
|
12
|
+
from aioia_core.settings import JWTSettings
|
|
13
|
+
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
|
13
14
|
from pydantic import BaseModel
|
|
14
15
|
from sqlalchemy.orm import sessionmaker
|
|
15
16
|
|
|
17
|
+
from aidol.api.common import register_image_generation_route
|
|
16
18
|
from aidol.protocols import (
|
|
17
19
|
AIdolRepositoryFactoryProtocol,
|
|
18
20
|
AIdolRepositoryProtocol,
|
|
19
21
|
ImageStorageProtocol,
|
|
20
22
|
)
|
|
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
|
|
23
|
+
from aidol.schemas import AIdol, AIdolCreate, AIdolPublic, AIdolUpdate
|
|
31
24
|
|
|
32
25
|
|
|
33
|
-
class
|
|
34
|
-
"""
|
|
26
|
+
class AIdolCreateResponse(BaseModel):
|
|
27
|
+
"""Response for AIdol creation (only id)."""
|
|
35
28
|
|
|
36
|
-
|
|
29
|
+
id: str
|
|
37
30
|
|
|
38
31
|
|
|
39
32
|
class AIdolRouter(
|
|
@@ -48,49 +41,92 @@ class AIdolRouter(
|
|
|
48
41
|
|
|
49
42
|
def __init__(
|
|
50
43
|
self,
|
|
51
|
-
|
|
44
|
+
google_api_key: str | None,
|
|
52
45
|
image_storage: ImageStorageProtocol,
|
|
53
46
|
**kwargs,
|
|
54
47
|
):
|
|
55
|
-
|
|
56
|
-
self.openai_settings = openai_settings
|
|
48
|
+
self.google_api_key = google_api_key
|
|
57
49
|
self.image_storage = image_storage
|
|
50
|
+
super().__init__(**kwargs)
|
|
58
51
|
|
|
59
52
|
def _register_routes(self) -> None:
|
|
60
53
|
"""Register routes (public CRUD + image generation)"""
|
|
61
|
-
|
|
54
|
+
# Register shared image generation route
|
|
55
|
+
register_image_generation_route(
|
|
56
|
+
router=self.router,
|
|
57
|
+
resource_name=self.resource_name,
|
|
58
|
+
google_api_key=self.google_api_key,
|
|
59
|
+
image_storage=self.image_storage,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
62
|
self._register_public_create_route()
|
|
63
63
|
self._register_public_get_route()
|
|
64
|
+
self._register_public_update_route()
|
|
65
|
+
|
|
66
|
+
def _register_public_update_route(self) -> None:
|
|
67
|
+
"""PATCH /{resource_name}/{id} - Update AIdol group (public)"""
|
|
68
|
+
|
|
69
|
+
@self.router.patch(
|
|
70
|
+
f"/{self.resource_name}/{{item_id}}",
|
|
71
|
+
response_model=AIdolPublic,
|
|
72
|
+
status_code=status.HTTP_200_OK,
|
|
73
|
+
summary="Update AIdol group",
|
|
74
|
+
description="Update AIdol group by ID (public endpoint). Returns updated AIdol data directly.",
|
|
75
|
+
responses={
|
|
76
|
+
404: {"model": ErrorResponse, "description": "AIdol group not found"},
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
async def update_aidol(
|
|
80
|
+
item_id: str,
|
|
81
|
+
data: AIdolUpdate,
|
|
82
|
+
repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
|
|
83
|
+
):
|
|
84
|
+
"""Update AIdol group."""
|
|
85
|
+
# TODO: Verify ClaimToken if strict ownership is required (Sprint 1)
|
|
86
|
+
updated = repository.update(item_id, data)
|
|
87
|
+
if not updated:
|
|
88
|
+
raise HTTPException(
|
|
89
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
90
|
+
detail="AIdol group not found",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Return updated AIdol as public schema
|
|
94
|
+
return AIdolPublic(**updated.model_dump())
|
|
64
95
|
|
|
65
96
|
def _register_public_create_route(self) -> None:
|
|
66
97
|
"""POST /{resource_name} - Create an AIdol group (public)"""
|
|
67
98
|
|
|
68
99
|
@self.router.post(
|
|
69
100
|
f"/{self.resource_name}",
|
|
70
|
-
response_model=
|
|
101
|
+
response_model=AIdolCreateResponse,
|
|
71
102
|
status_code=status.HTTP_201_CREATED,
|
|
72
103
|
summary="Create AIdol group",
|
|
73
|
-
description="Create a new AIdol group (public endpoint)",
|
|
104
|
+
description="Create a new AIdol group (public endpoint). Returns only the created id.",
|
|
74
105
|
)
|
|
75
106
|
async def create_aidol(
|
|
76
107
|
request: AIdolCreate,
|
|
108
|
+
response: Response,
|
|
77
109
|
repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
|
|
78
110
|
):
|
|
79
111
|
"""Create a new AIdol group."""
|
|
80
112
|
created = repository.create(request)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
|
|
114
|
+
# Set ClaimToken header
|
|
115
|
+
if created.claim_token:
|
|
116
|
+
response.headers["ClaimToken"] = created.claim_token
|
|
117
|
+
|
|
118
|
+
# Return only id
|
|
119
|
+
return AIdolCreateResponse(id=created.id)
|
|
84
120
|
|
|
85
121
|
def _register_public_get_route(self) -> None:
|
|
86
122
|
"""GET /{resource_name}/{id} - Get an AIdol group (public)"""
|
|
87
123
|
|
|
88
124
|
@self.router.get(
|
|
89
125
|
f"/{self.resource_name}/{{item_id}}",
|
|
90
|
-
response_model=
|
|
126
|
+
response_model=AIdolPublic,
|
|
91
127
|
status_code=status.HTTP_200_OK,
|
|
92
128
|
summary="Get AIdol group",
|
|
93
|
-
description="Get AIdol group by ID (public endpoint)",
|
|
129
|
+
description="Get AIdol group by ID (public endpoint). Returns AIdol data directly.",
|
|
94
130
|
responses={
|
|
95
131
|
404: {"model": ErrorResponse, "description": "AIdol group not found"},
|
|
96
132
|
},
|
|
@@ -101,54 +137,12 @@ class AIdolRouter(
|
|
|
101
137
|
):
|
|
102
138
|
"""Get AIdol group by ID."""
|
|
103
139
|
aidol = self._get_item_or_404(repository, item_id)
|
|
104
|
-
#
|
|
105
|
-
|
|
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
|
-
)
|
|
140
|
+
# Return AIdol as public schema
|
|
141
|
+
return AIdolPublic(**aidol.model_dump())
|
|
148
142
|
|
|
149
143
|
|
|
150
144
|
def create_aidol_router(
|
|
151
|
-
|
|
145
|
+
google_api_key: str | None,
|
|
152
146
|
db_session_factory: sessionmaker,
|
|
153
147
|
repository_factory: AIdolRepositoryFactoryProtocol,
|
|
154
148
|
image_storage: ImageStorageProtocol,
|
|
@@ -161,7 +155,7 @@ def create_aidol_router(
|
|
|
161
155
|
Create AIdol router with dependency injection.
|
|
162
156
|
|
|
163
157
|
Args:
|
|
164
|
-
|
|
158
|
+
google_api_key: Google API Key for image generation
|
|
165
159
|
db_session_factory: Database session factory
|
|
166
160
|
repository_factory: Factory implementing AIdolRepositoryFactoryProtocol
|
|
167
161
|
image_storage: Image storage for permanent URLs
|
|
@@ -174,7 +168,7 @@ def create_aidol_router(
|
|
|
174
168
|
FastAPI APIRouter instance
|
|
175
169
|
"""
|
|
176
170
|
router = AIdolRouter(
|
|
177
|
-
|
|
171
|
+
google_api_key=google_api_key,
|
|
178
172
|
image_storage=image_storage,
|
|
179
173
|
model_class=AIdol,
|
|
180
174
|
create_schema=AIdolCreate,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common API utilities.
|
|
3
|
+
|
|
4
|
+
Shared functions for registering common routes across different routers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from aioia_core.errors import ErrorResponse
|
|
8
|
+
from fastapi import APIRouter, HTTPException, status
|
|
9
|
+
|
|
10
|
+
from aidol.protocols import ImageStorageProtocol
|
|
11
|
+
from aidol.schemas import (
|
|
12
|
+
ImageGenerationData,
|
|
13
|
+
ImageGenerationRequest,
|
|
14
|
+
ImageGenerationResponse,
|
|
15
|
+
)
|
|
16
|
+
from aidol.services import ImageGenerationService
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register_image_generation_route(
|
|
20
|
+
router: APIRouter,
|
|
21
|
+
resource_name: str,
|
|
22
|
+
google_api_key: str | None,
|
|
23
|
+
image_storage: ImageStorageProtocol,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Register image generation route to the given router.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
router: FastAPI APIRouter instance
|
|
30
|
+
resource_name: Resource name for the route path
|
|
31
|
+
google_api_key: Google API Key
|
|
32
|
+
image_storage: Image Storage instance
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@router.post(
|
|
36
|
+
f"/{resource_name}/images",
|
|
37
|
+
response_model=ImageGenerationResponse,
|
|
38
|
+
status_code=status.HTTP_201_CREATED,
|
|
39
|
+
summary="Generate image",
|
|
40
|
+
description=f"Generate image for {resource_name}",
|
|
41
|
+
responses={
|
|
42
|
+
500: {"model": ErrorResponse, "description": "Image generation failed"},
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
async def generate_image(request: ImageGenerationRequest):
|
|
46
|
+
"""Generate image from prompt."""
|
|
47
|
+
# Generate and download image
|
|
48
|
+
service = ImageGenerationService(api_key=google_api_key)
|
|
49
|
+
image = service.generate_and_download_image(
|
|
50
|
+
prompt=request.prompt,
|
|
51
|
+
size="1024x1024",
|
|
52
|
+
quality="standard",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if image is None:
|
|
56
|
+
raise HTTPException(
|
|
57
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
58
|
+
detail="Image generation failed",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Upload to permanent storage
|
|
62
|
+
image_url = image_storage.upload_image(image)
|
|
63
|
+
|
|
64
|
+
return ImageGenerationResponse(
|
|
65
|
+
data=ImageGenerationData(
|
|
66
|
+
image_url=image_url,
|
|
67
|
+
width=1024,
|
|
68
|
+
height=1024,
|
|
69
|
+
format="png",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# pylint: disable=duplicate-code
|
|
2
|
+
"""
|
|
3
|
+
Companion API router
|
|
4
|
+
|
|
5
|
+
Public endpoints for Companion creation and retrieval.
|
|
6
|
+
Public access pattern: no authentication required.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from aioia_core.auth import UserInfoProvider
|
|
10
|
+
from aioia_core.errors import ErrorResponse
|
|
11
|
+
from aioia_core.fastapi import BaseCrudRouter
|
|
12
|
+
from aioia_core.settings import JWTSettings
|
|
13
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
from sqlalchemy.orm import sessionmaker
|
|
16
|
+
|
|
17
|
+
from aidol.api.common import register_image_generation_route
|
|
18
|
+
from aidol.protocols import (
|
|
19
|
+
CompanionRepositoryFactoryProtocol,
|
|
20
|
+
CompanionRepositoryProtocol,
|
|
21
|
+
ImageStorageProtocol,
|
|
22
|
+
)
|
|
23
|
+
from aidol.schemas import (
|
|
24
|
+
Companion,
|
|
25
|
+
CompanionCreate,
|
|
26
|
+
CompanionPublic,
|
|
27
|
+
CompanionUpdate,
|
|
28
|
+
Gender,
|
|
29
|
+
)
|
|
30
|
+
from aidol.services.companion_service import to_companion_public
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CompanionPaginatedResponse(BaseModel):
|
|
34
|
+
"""Paginated response for Companion (public)."""
|
|
35
|
+
|
|
36
|
+
data: list[CompanionPublic]
|
|
37
|
+
total: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CompanionRouter(
|
|
41
|
+
BaseCrudRouter[
|
|
42
|
+
Companion, CompanionCreate, CompanionUpdate, CompanionRepositoryProtocol
|
|
43
|
+
]
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Companion router with public endpoints.
|
|
47
|
+
|
|
48
|
+
Public CRUD pattern: no authentication required.
|
|
49
|
+
Returns CompanionPublic (excludes system_prompt) for all responses.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
google_api_key: str | None,
|
|
55
|
+
image_storage: ImageStorageProtocol,
|
|
56
|
+
**kwargs,
|
|
57
|
+
):
|
|
58
|
+
self.google_api_key = google_api_key
|
|
59
|
+
self.image_storage = image_storage
|
|
60
|
+
super().__init__(**kwargs)
|
|
61
|
+
|
|
62
|
+
def _register_routes(self) -> None:
|
|
63
|
+
"""Register routes (public CRUD + image generation)"""
|
|
64
|
+
# Register shared image generation route
|
|
65
|
+
register_image_generation_route(
|
|
66
|
+
router=self.router,
|
|
67
|
+
resource_name=self.resource_name,
|
|
68
|
+
google_api_key=self.google_api_key,
|
|
69
|
+
image_storage=self.image_storage,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self._register_public_list_route()
|
|
73
|
+
self._register_public_create_route()
|
|
74
|
+
self._register_public_get_route()
|
|
75
|
+
self._register_public_update_route()
|
|
76
|
+
self._register_public_delete_route()
|
|
77
|
+
|
|
78
|
+
def _register_public_list_route(self) -> None:
|
|
79
|
+
"""GET /{resource_name} - List Companions (public)"""
|
|
80
|
+
|
|
81
|
+
@self.router.get(
|
|
82
|
+
f"/{self.resource_name}",
|
|
83
|
+
response_model=CompanionPaginatedResponse,
|
|
84
|
+
status_code=status.HTTP_200_OK,
|
|
85
|
+
summary="List Companions",
|
|
86
|
+
description="List all Companions with optional filtering by gender and cast status",
|
|
87
|
+
)
|
|
88
|
+
async def list_companions(
|
|
89
|
+
gender: Gender | None = Query(None, description="Filter by gender"),
|
|
90
|
+
is_cast: bool | None = Query(
|
|
91
|
+
None, alias="isCast", description="Filter by cast status"
|
|
92
|
+
),
|
|
93
|
+
aidol_id: str | None = Query(None, description="Filter by AIdol Group ID"),
|
|
94
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
95
|
+
):
|
|
96
|
+
"""List Companions with optional gender and isCast filters."""
|
|
97
|
+
filter_list: list[dict] = []
|
|
98
|
+
|
|
99
|
+
# Add filters only if provided
|
|
100
|
+
if gender is not None:
|
|
101
|
+
filter_list.append(
|
|
102
|
+
{"field": "gender", "operator": "eq", "value": gender.value}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Filter by aidol_id if provided
|
|
106
|
+
if aidol_id is not None:
|
|
107
|
+
filter_list.append(
|
|
108
|
+
{"field": "aidol_id", "operator": "eq", "value": aidol_id}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# isCast is derived from aidol_id presence
|
|
112
|
+
# isCast=true → aidol_id is not null (belongs to a group)
|
|
113
|
+
# isCast=false → aidol_id is null (not in a group)
|
|
114
|
+
if is_cast is True:
|
|
115
|
+
filter_list.append(
|
|
116
|
+
{"field": "aidol_id", "operator": "ne", "value": None}
|
|
117
|
+
)
|
|
118
|
+
elif is_cast is False:
|
|
119
|
+
filter_list.append(
|
|
120
|
+
{"field": "aidol_id", "operator": "eq", "value": None}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
items, total = repository.get_all(
|
|
124
|
+
filters=filter_list if filter_list else None,
|
|
125
|
+
)
|
|
126
|
+
# Convert to Public schema (exclude system_prompt)
|
|
127
|
+
public_items = [to_companion_public(c) for c in items]
|
|
128
|
+
return CompanionPaginatedResponse(data=public_items, total=total)
|
|
129
|
+
|
|
130
|
+
def _register_public_create_route(self) -> None:
|
|
131
|
+
"""POST /{resource_name} - Create a Companion (public)"""
|
|
132
|
+
|
|
133
|
+
@self.router.post(
|
|
134
|
+
f"/{self.resource_name}",
|
|
135
|
+
response_model=CompanionPublic,
|
|
136
|
+
status_code=status.HTTP_201_CREATED,
|
|
137
|
+
summary="Create Companion",
|
|
138
|
+
description="Create a new Companion. Returns the created companion data.",
|
|
139
|
+
)
|
|
140
|
+
async def create_companion(
|
|
141
|
+
request: CompanionCreate,
|
|
142
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
143
|
+
):
|
|
144
|
+
"""Create a new Companion."""
|
|
145
|
+
# Exclude system_prompt from request - should not be set via API
|
|
146
|
+
sanitized_data = request.model_dump(exclude={"system_prompt"})
|
|
147
|
+
sanitized_request = CompanionCreate(**sanitized_data)
|
|
148
|
+
|
|
149
|
+
created = repository.create(sanitized_request)
|
|
150
|
+
# Return created companion as public schema
|
|
151
|
+
return to_companion_public(created)
|
|
152
|
+
|
|
153
|
+
def _register_public_get_route(self) -> None:
|
|
154
|
+
"""GET /{resource_name}/{id} - Get a Companion (public)"""
|
|
155
|
+
|
|
156
|
+
@self.router.get(
|
|
157
|
+
f"/{self.resource_name}/{{item_id}}",
|
|
158
|
+
response_model=CompanionPublic,
|
|
159
|
+
status_code=status.HTTP_200_OK,
|
|
160
|
+
summary="Get Companion",
|
|
161
|
+
description="Get Companion by ID (public endpoint). Returns companion data directly.",
|
|
162
|
+
responses={
|
|
163
|
+
404: {"model": ErrorResponse, "description": "Companion not found"},
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
async def get_companion(
|
|
167
|
+
item_id: str,
|
|
168
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
169
|
+
):
|
|
170
|
+
"""Get Companion by ID."""
|
|
171
|
+
companion = self._get_item_or_404(repository, item_id)
|
|
172
|
+
# Return companion as public schema
|
|
173
|
+
return to_companion_public(companion)
|
|
174
|
+
|
|
175
|
+
def _register_public_update_route(self) -> None:
|
|
176
|
+
"""PATCH /{resource_name}/{id} - Update Companion (public)"""
|
|
177
|
+
|
|
178
|
+
@self.router.patch(
|
|
179
|
+
f"/{self.resource_name}/{{item_id}}",
|
|
180
|
+
response_model=CompanionPublic,
|
|
181
|
+
status_code=status.HTTP_200_OK,
|
|
182
|
+
summary="Update Companion",
|
|
183
|
+
description="Update Companion by ID (public endpoint). Returns updated companion data directly.",
|
|
184
|
+
responses={
|
|
185
|
+
404: {"model": ErrorResponse, "description": "Companion not found"},
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
async def update_companion(
|
|
189
|
+
item_id: str,
|
|
190
|
+
data: CompanionUpdate,
|
|
191
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
192
|
+
):
|
|
193
|
+
"""Update Companion."""
|
|
194
|
+
# Exclude system_prompt from request - should not be set via API
|
|
195
|
+
sanitized_data = data.model_dump(
|
|
196
|
+
exclude={"system_prompt"}, exclude_unset=True
|
|
197
|
+
)
|
|
198
|
+
sanitized_request = CompanionUpdate(**sanitized_data)
|
|
199
|
+
|
|
200
|
+
updated = repository.update(item_id, sanitized_request)
|
|
201
|
+
if not updated:
|
|
202
|
+
raise HTTPException(
|
|
203
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
204
|
+
detail="Companion not found",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Return updated companion as public schema
|
|
208
|
+
return to_companion_public(updated)
|
|
209
|
+
|
|
210
|
+
def _register_public_delete_route(self) -> None:
|
|
211
|
+
"""DELETE /{resource_name}/{id} - Remove Companion from Group (public)"""
|
|
212
|
+
|
|
213
|
+
@self.router.delete(
|
|
214
|
+
f"/{self.resource_name}/{{item_id}}",
|
|
215
|
+
response_model=CompanionPublic,
|
|
216
|
+
status_code=status.HTTP_200_OK,
|
|
217
|
+
summary="Remove Companion from Group",
|
|
218
|
+
description="Remove Companion from AIdol group (unassign aidol_id). Does not delete the record.",
|
|
219
|
+
responses={
|
|
220
|
+
404: {"model": ErrorResponse, "description": "Companion not found"},
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
async def delete_companion(
|
|
224
|
+
item_id: str,
|
|
225
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
226
|
+
):
|
|
227
|
+
"""Remove Companion from Group (Unassign)."""
|
|
228
|
+
# Get item first
|
|
229
|
+
self._get_item_or_404(repository, item_id)
|
|
230
|
+
|
|
231
|
+
# Update aidol_id to None (remove from group)
|
|
232
|
+
update_data = CompanionUpdate(aidol_id=None)
|
|
233
|
+
|
|
234
|
+
updated = repository.update(item_id, update_data)
|
|
235
|
+
|
|
236
|
+
if not updated:
|
|
237
|
+
raise HTTPException(
|
|
238
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
239
|
+
detail="Companion not found",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Return updated companion as public schema
|
|
243
|
+
return to_companion_public(updated)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def create_companion_router(
|
|
247
|
+
google_api_key: str | None,
|
|
248
|
+
db_session_factory: sessionmaker,
|
|
249
|
+
repository_factory: CompanionRepositoryFactoryProtocol,
|
|
250
|
+
image_storage: ImageStorageProtocol,
|
|
251
|
+
jwt_settings: JWTSettings | None = None,
|
|
252
|
+
user_info_provider: UserInfoProvider | None = None,
|
|
253
|
+
resource_name: str = "companions",
|
|
254
|
+
tags: list[str] | None = None,
|
|
255
|
+
) -> APIRouter:
|
|
256
|
+
"""
|
|
257
|
+
Create Companion router with dependency injection.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
google_api_key: Google API Key for image generation
|
|
261
|
+
db_session_factory: Database session factory
|
|
262
|
+
repository_factory: Factory implementing CompanionRepositoryFactoryProtocol
|
|
263
|
+
image_storage: Image storage for permanent URLs
|
|
264
|
+
jwt_settings: Optional JWT settings for authentication
|
|
265
|
+
user_info_provider: Optional user info provider
|
|
266
|
+
resource_name: Resource name for routes (default: "companions")
|
|
267
|
+
tags: Optional OpenAPI tags
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
FastAPI APIRouter instance
|
|
271
|
+
"""
|
|
272
|
+
router = CompanionRouter(
|
|
273
|
+
google_api_key=google_api_key,
|
|
274
|
+
image_storage=image_storage,
|
|
275
|
+
model_class=Companion,
|
|
276
|
+
create_schema=CompanionCreate,
|
|
277
|
+
update_schema=CompanionUpdate,
|
|
278
|
+
db_session_factory=db_session_factory,
|
|
279
|
+
repository_factory=repository_factory,
|
|
280
|
+
user_info_provider=user_info_provider,
|
|
281
|
+
jwt_secret_key=jwt_settings.secret_key if jwt_settings else None,
|
|
282
|
+
resource_name=resource_name,
|
|
283
|
+
tags=tags or ["Companion"],
|
|
284
|
+
)
|
|
285
|
+
return router.get_router()
|