py-aidol 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- aidol/api/aidol.py +67 -73
- aidol/api/common.py +71 -0
- aidol/api/companion.py +156 -39
- aidol/api/lead.py +123 -0
- aidol/factories.py +8 -0
- aidol/models/__init__.py +2 -1
- aidol/models/aidol.py +4 -2
- aidol/models/aidol_lead.py +22 -0
- aidol/models/companion.py +20 -2
- aidol/protocols.py +26 -0
- aidol/repositories/__init__.py +2 -0
- aidol/repositories/aidol.py +2 -0
- aidol/repositories/aidol_lead.py +49 -0
- aidol/repositories/companion.py +38 -5
- aidol/schemas/__init__.py +12 -0
- aidol/schemas/aidol.py +10 -6
- aidol/schemas/aidol_lead.py +38 -0
- aidol/schemas/companion.py +122 -5
- aidol/services/companion_service.py +96 -0
- aidol/services/image_generation_service.py +56 -101
- {py_aidol-0.1.0.dist-info → py_aidol-0.3.0.dist-info}/METADATA +2 -1
- py_aidol-0.3.0.dist-info/RECORD +27 -0
- py_aidol-0.1.0.dist-info/RECORD +0 -21
- {py_aidol-0.1.0.dist-info → py_aidol-0.3.0.dist-info}/WHEEL +0 -0
aidol/api/aidol.py
CHANGED
|
@@ -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,
|
aidol/api/common.py
ADDED
|
@@ -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
|
+
)
|
aidol/api/companion.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pylint: disable=duplicate-code
|
|
1
2
|
"""
|
|
2
3
|
Companion API router
|
|
3
4
|
|
|
@@ -9,21 +10,24 @@ from aioia_core.auth import UserInfoProvider
|
|
|
9
10
|
from aioia_core.errors import ErrorResponse
|
|
10
11
|
from aioia_core.fastapi import BaseCrudRouter
|
|
11
12
|
from aioia_core.settings import JWTSettings
|
|
12
|
-
from fastapi import APIRouter, Depends, Query, status
|
|
13
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, 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
|
CompanionRepositoryFactoryProtocol,
|
|
18
20
|
CompanionRepositoryProtocol,
|
|
21
|
+
ImageStorageProtocol,
|
|
19
22
|
)
|
|
20
|
-
from aidol.schemas import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
class CompanionPaginatedResponse(BaseModel):
|
|
@@ -45,11 +49,31 @@ class CompanionRouter(
|
|
|
45
49
|
Returns CompanionPublic (excludes system_prompt) for all responses.
|
|
46
50
|
"""
|
|
47
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
|
+
|
|
48
62
|
def _register_routes(self) -> None:
|
|
49
|
-
"""Register routes (public CRUD)"""
|
|
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
|
+
|
|
50
72
|
self._register_public_list_route()
|
|
51
73
|
self._register_public_create_route()
|
|
52
74
|
self._register_public_get_route()
|
|
75
|
+
self._register_public_update_route()
|
|
76
|
+
self._register_public_delete_route()
|
|
53
77
|
|
|
54
78
|
def _register_public_list_route(self) -> None:
|
|
55
79
|
"""GET /{resource_name} - List Companions (public)"""
|
|
@@ -59,33 +83,48 @@ class CompanionRouter(
|
|
|
59
83
|
response_model=CompanionPaginatedResponse,
|
|
60
84
|
status_code=status.HTTP_200_OK,
|
|
61
85
|
summary="List Companions",
|
|
62
|
-
description="List all Companions with optional filtering
|
|
86
|
+
description="List all Companions with optional filtering by gender and cast status",
|
|
63
87
|
)
|
|
64
88
|
async def list_companions(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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)",
|
|
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"
|
|
76
92
|
),
|
|
93
|
+
aidol_id: str | None = Query(None, description="Filter by AIdol Group ID"),
|
|
77
94
|
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
78
95
|
):
|
|
79
|
-
"""List Companions with
|
|
80
|
-
|
|
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
|
+
|
|
81
123
|
items, total = repository.get_all(
|
|
82
|
-
|
|
83
|
-
page_size=page_size,
|
|
84
|
-
sort=sort_list,
|
|
85
|
-
filters=filter_list,
|
|
124
|
+
filters=filter_list if filter_list else None,
|
|
86
125
|
)
|
|
87
126
|
# Convert to Public schema (exclude system_prompt)
|
|
88
|
-
public_items = [
|
|
127
|
+
public_items = [to_companion_public(c) for c in items]
|
|
89
128
|
return CompanionPaginatedResponse(data=public_items, total=total)
|
|
90
129
|
|
|
91
130
|
def _register_public_create_route(self) -> None:
|
|
@@ -93,30 +132,33 @@ class CompanionRouter(
|
|
|
93
132
|
|
|
94
133
|
@self.router.post(
|
|
95
134
|
f"/{self.resource_name}",
|
|
96
|
-
response_model=
|
|
135
|
+
response_model=CompanionPublic,
|
|
97
136
|
status_code=status.HTTP_201_CREATED,
|
|
98
137
|
summary="Create Companion",
|
|
99
|
-
description="Create a new Companion
|
|
138
|
+
description="Create a new Companion. Returns the created companion data.",
|
|
100
139
|
)
|
|
101
140
|
async def create_companion(
|
|
102
141
|
request: CompanionCreate,
|
|
103
142
|
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
104
143
|
):
|
|
105
144
|
"""Create a new Companion."""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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)
|
|
110
152
|
|
|
111
153
|
def _register_public_get_route(self) -> None:
|
|
112
154
|
"""GET /{resource_name}/{id} - Get a Companion (public)"""
|
|
113
155
|
|
|
114
156
|
@self.router.get(
|
|
115
157
|
f"/{self.resource_name}/{{item_id}}",
|
|
116
|
-
response_model=
|
|
158
|
+
response_model=CompanionPublic,
|
|
117
159
|
status_code=status.HTTP_200_OK,
|
|
118
160
|
summary="Get Companion",
|
|
119
|
-
description="Get Companion by ID (public endpoint)",
|
|
161
|
+
description="Get Companion by ID (public endpoint). Returns companion data directly.",
|
|
120
162
|
responses={
|
|
121
163
|
404: {"model": ErrorResponse, "description": "Companion not found"},
|
|
122
164
|
},
|
|
@@ -127,14 +169,85 @@ class CompanionRouter(
|
|
|
127
169
|
):
|
|
128
170
|
"""Get Companion by ID."""
|
|
129
171
|
companion = self._get_item_or_404(repository, item_id)
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
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)
|
|
133
244
|
|
|
134
245
|
|
|
135
246
|
def create_companion_router(
|
|
247
|
+
google_api_key: str | None,
|
|
136
248
|
db_session_factory: sessionmaker,
|
|
137
249
|
repository_factory: CompanionRepositoryFactoryProtocol,
|
|
250
|
+
image_storage: ImageStorageProtocol,
|
|
138
251
|
jwt_settings: JWTSettings | None = None,
|
|
139
252
|
user_info_provider: UserInfoProvider | None = None,
|
|
140
253
|
resource_name: str = "companions",
|
|
@@ -144,8 +257,10 @@ def create_companion_router(
|
|
|
144
257
|
Create Companion router with dependency injection.
|
|
145
258
|
|
|
146
259
|
Args:
|
|
260
|
+
google_api_key: Google API Key for image generation
|
|
147
261
|
db_session_factory: Database session factory
|
|
148
262
|
repository_factory: Factory implementing CompanionRepositoryFactoryProtocol
|
|
263
|
+
image_storage: Image storage for permanent URLs
|
|
149
264
|
jwt_settings: Optional JWT settings for authentication
|
|
150
265
|
user_info_provider: Optional user info provider
|
|
151
266
|
resource_name: Resource name for routes (default: "companions")
|
|
@@ -155,6 +270,8 @@ def create_companion_router(
|
|
|
155
270
|
FastAPI APIRouter instance
|
|
156
271
|
"""
|
|
157
272
|
router = CompanionRouter(
|
|
273
|
+
google_api_key=google_api_key,
|
|
274
|
+
image_storage=image_storage,
|
|
158
275
|
model_class=Companion,
|
|
159
276
|
create_schema=CompanionCreate,
|
|
160
277
|
update_schema=CompanionUpdate,
|