py-aidol 0.2.0__py3-none-any.whl → 0.4.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 CHANGED
@@ -1,3 +1,4 @@
1
+ # pylint: disable=duplicate-code
1
2
  """
2
3
  AIdol API router
3
4
 
@@ -8,32 +9,25 @@ 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, OpenAIAPISettings
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
24
+ from aidol.settings import GoogleGenAISettings
31
25
 
32
26
 
33
- class AIdolSingleItemResponse(BaseModel):
34
- """Single item response for AIdol (public)."""
27
+ class AIdolCreateResponse(BaseModel):
28
+ """Response for AIdol creation (only id)."""
35
29
 
36
- data: AIdolPublic
30
+ id: str
37
31
 
38
32
 
39
33
  class AIdolRouter(
@@ -48,49 +42,92 @@ class AIdolRouter(
48
42
 
49
43
  def __init__(
50
44
  self,
51
- openai_settings: OpenAIAPISettings,
45
+ google_settings: GoogleGenAISettings | None,
52
46
  image_storage: ImageStorageProtocol,
53
47
  **kwargs,
54
48
  ):
55
- super().__init__(**kwargs)
56
- self.openai_settings = openai_settings
49
+ self.google_settings = google_settings
57
50
  self.image_storage = image_storage
51
+ super().__init__(**kwargs)
58
52
 
59
53
  def _register_routes(self) -> None:
60
54
  """Register routes (public CRUD + image generation)"""
61
- self._register_image_generation_route()
55
+ # Register shared image generation route
56
+ register_image_generation_route(
57
+ router=self.router,
58
+ resource_name=self.resource_name,
59
+ google_settings=self.google_settings,
60
+ image_storage=self.image_storage,
61
+ )
62
+
62
63
  self._register_public_create_route()
63
64
  self._register_public_get_route()
65
+ self._register_public_update_route()
66
+
67
+ def _register_public_update_route(self) -> None:
68
+ """PATCH /{resource_name}/{id} - Update AIdol group (public)"""
69
+
70
+ @self.router.patch(
71
+ f"/{self.resource_name}/{{item_id}}",
72
+ response_model=AIdolPublic,
73
+ status_code=status.HTTP_200_OK,
74
+ summary="Update AIdol group",
75
+ description="Update AIdol group by ID (public endpoint). Returns updated AIdol data directly.",
76
+ responses={
77
+ 404: {"model": ErrorResponse, "description": "AIdol group not found"},
78
+ },
79
+ )
80
+ async def update_aidol(
81
+ item_id: str,
82
+ data: AIdolUpdate,
83
+ repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
84
+ ):
85
+ """Update AIdol group."""
86
+ # TODO: Verify ClaimToken if strict ownership is required (Sprint 1)
87
+ updated = repository.update(item_id, data)
88
+ if not updated:
89
+ raise HTTPException(
90
+ status_code=status.HTTP_404_NOT_FOUND,
91
+ detail="AIdol group not found",
92
+ )
93
+
94
+ # Return updated AIdol as public schema
95
+ return AIdolPublic(**updated.model_dump())
64
96
 
65
97
  def _register_public_create_route(self) -> None:
66
98
  """POST /{resource_name} - Create an AIdol group (public)"""
67
99
 
68
100
  @self.router.post(
69
101
  f"/{self.resource_name}",
70
- response_model=AIdolSingleItemResponse,
102
+ response_model=AIdolCreateResponse,
71
103
  status_code=status.HTTP_201_CREATED,
72
104
  summary="Create AIdol group",
73
- description="Create a new AIdol group (public endpoint)",
105
+ description="Create a new AIdol group (public endpoint). Returns only the created id.",
74
106
  )
75
107
  async def create_aidol(
76
108
  request: AIdolCreate,
109
+ response: Response,
77
110
  repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
78
111
  ):
79
112
  """Create a new AIdol group."""
80
113
  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)
114
+
115
+ # Set ClaimToken header
116
+ if created.claim_token:
117
+ response.headers["ClaimToken"] = created.claim_token
118
+
119
+ # Return only id
120
+ return AIdolCreateResponse(id=created.id)
84
121
 
85
122
  def _register_public_get_route(self) -> None:
86
123
  """GET /{resource_name}/{id} - Get an AIdol group (public)"""
87
124
 
88
125
  @self.router.get(
89
126
  f"/{self.resource_name}/{{item_id}}",
90
- response_model=AIdolSingleItemResponse,
127
+ response_model=AIdolPublic,
91
128
  status_code=status.HTTP_200_OK,
92
129
  summary="Get AIdol group",
93
- description="Get AIdol group by ID (public endpoint)",
130
+ description="Get AIdol group by ID (public endpoint). Returns AIdol data directly.",
94
131
  responses={
95
132
  404: {"model": ErrorResponse, "description": "AIdol group not found"},
96
133
  },
@@ -101,54 +138,12 @@ class AIdolRouter(
101
138
  ):
102
139
  """Get AIdol group by ID."""
103
140
  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
- )
141
+ # Return AIdol as public schema
142
+ return AIdolPublic(**aidol.model_dump())
148
143
 
149
144
 
150
145
  def create_aidol_router(
151
- openai_settings: OpenAIAPISettings,
146
+ google_settings: GoogleGenAISettings | None,
152
147
  db_session_factory: sessionmaker,
153
148
  repository_factory: AIdolRepositoryFactoryProtocol,
154
149
  image_storage: ImageStorageProtocol,
@@ -161,7 +156,7 @@ def create_aidol_router(
161
156
  Create AIdol router with dependency injection.
162
157
 
163
158
  Args:
164
- openai_settings: OpenAI API settings for image generation
159
+ google_settings: Google API settings (uses ADC if api_key is None)
165
160
  db_session_factory: Database session factory
166
161
  repository_factory: Factory implementing AIdolRepositoryFactoryProtocol
167
162
  image_storage: Image storage for permanent URLs
@@ -174,7 +169,7 @@ def create_aidol_router(
174
169
  FastAPI APIRouter instance
175
170
  """
176
171
  router = AIdolRouter(
177
- openai_settings=openai_settings,
172
+ google_settings=google_settings,
178
173
  image_storage=image_storage,
179
174
  model_class=AIdol,
180
175
  create_schema=AIdolCreate,
aidol/api/common.py ADDED
@@ -0,0 +1,72 @@
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
+ from aidol.settings import GoogleGenAISettings
18
+
19
+
20
+ def register_image_generation_route(
21
+ router: APIRouter,
22
+ resource_name: str,
23
+ google_settings: GoogleGenAISettings | None,
24
+ image_storage: ImageStorageProtocol,
25
+ ) -> None:
26
+ """
27
+ Register image generation route to the given router.
28
+
29
+ Args:
30
+ router: FastAPI APIRouter instance
31
+ resource_name: Resource name for the route path
32
+ google_settings: Google API settings (API Key or Vertex AI with ADC)
33
+ image_storage: Image Storage instance
34
+ """
35
+
36
+ @router.post(
37
+ f"/{resource_name}/images",
38
+ response_model=ImageGenerationResponse,
39
+ status_code=status.HTTP_201_CREATED,
40
+ summary="Generate image",
41
+ description=f"Generate image for {resource_name}",
42
+ responses={
43
+ 500: {"model": ErrorResponse, "description": "Image generation failed"},
44
+ },
45
+ )
46
+ async def generate_image(request: ImageGenerationRequest):
47
+ """Generate image from prompt."""
48
+ # Generate and download image
49
+ service = ImageGenerationService(settings=google_settings)
50
+ image = service.generate_and_download_image(
51
+ prompt=request.prompt,
52
+ size="1024x1024",
53
+ quality="standard",
54
+ )
55
+
56
+ if image is None:
57
+ raise HTTPException(
58
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
59
+ detail="Image generation failed",
60
+ )
61
+
62
+ # Upload to permanent storage
63
+ image_url = image_storage.upload_image(image)
64
+
65
+ return ImageGenerationResponse(
66
+ data=ImageGenerationData(
67
+ image_url=image_url,
68
+ width=1024,
69
+ height=1024,
70
+ format="png",
71
+ )
72
+ )
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,25 @@ 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 Companion, CompanionCreate, CompanionPublic, CompanionUpdate
21
-
22
-
23
- class CompanionSingleItemResponse(BaseModel):
24
- """Single item response for Companion (public)."""
25
-
26
- data: CompanionPublic
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
+ from aidol.settings import GoogleGenAISettings
27
32
 
28
33
 
29
34
  class CompanionPaginatedResponse(BaseModel):
@@ -45,11 +50,31 @@ class CompanionRouter(
45
50
  Returns CompanionPublic (excludes system_prompt) for all responses.
46
51
  """
47
52
 
53
+ def __init__(
54
+ self,
55
+ google_settings: GoogleGenAISettings | None,
56
+ image_storage: ImageStorageProtocol,
57
+ **kwargs,
58
+ ):
59
+ self.google_settings = google_settings
60
+ self.image_storage = image_storage
61
+ super().__init__(**kwargs)
62
+
48
63
  def _register_routes(self) -> None:
49
- """Register routes (public CRUD)"""
64
+ """Register routes (public CRUD + image generation)"""
65
+ # Register shared image generation route
66
+ register_image_generation_route(
67
+ router=self.router,
68
+ resource_name=self.resource_name,
69
+ google_settings=self.google_settings,
70
+ image_storage=self.image_storage,
71
+ )
72
+
50
73
  self._register_public_list_route()
51
74
  self._register_public_create_route()
52
75
  self._register_public_get_route()
76
+ self._register_public_update_route()
77
+ self._register_public_delete_route()
53
78
 
54
79
  def _register_public_list_route(self) -> None:
55
80
  """GET /{resource_name} - List Companions (public)"""
@@ -59,33 +84,48 @@ class CompanionRouter(
59
84
  response_model=CompanionPaginatedResponse,
60
85
  status_code=status.HTTP_200_OK,
61
86
  summary="List Companions",
62
- description="List all Companions with optional filtering (public endpoint)",
87
+ description="List all Companions with optional filtering by gender and cast status",
63
88
  )
64
89
  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)",
90
+ gender: Gender | None = Query(None, description="Filter by gender"),
91
+ is_cast: bool | None = Query(
92
+ None, alias="isCast", description="Filter by cast status"
76
93
  ),
94
+ aidol_id: str | None = Query(None, description="Filter by AIdol Group ID"),
77
95
  repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
78
96
  ):
79
- """List Companions with pagination, sorting, and filtering."""
80
- sort_list, filter_list = self._parse_query_params(sort_param, filters_param)
97
+ """List Companions with optional gender and isCast filters."""
98
+ filter_list: list[dict] = []
99
+
100
+ # Add filters only if provided
101
+ if gender is not None:
102
+ filter_list.append(
103
+ {"field": "gender", "operator": "eq", "value": gender.value}
104
+ )
105
+
106
+ # Filter by aidol_id if provided
107
+ if aidol_id is not None:
108
+ filter_list.append(
109
+ {"field": "aidol_id", "operator": "eq", "value": aidol_id}
110
+ )
111
+
112
+ # isCast is derived from aidol_id presence
113
+ # isCast=true → aidol_id is not null (belongs to a group)
114
+ # isCast=false → aidol_id is null (not in a group)
115
+ if is_cast is True:
116
+ filter_list.append(
117
+ {"field": "aidol_id", "operator": "ne", "value": None}
118
+ )
119
+ elif is_cast is False:
120
+ filter_list.append(
121
+ {"field": "aidol_id", "operator": "eq", "value": None}
122
+ )
123
+
81
124
  items, total = repository.get_all(
82
- current=current,
83
- page_size=page_size,
84
- sort=sort_list,
85
- filters=filter_list,
125
+ filters=filter_list if filter_list else None,
86
126
  )
87
127
  # Convert to Public schema (exclude system_prompt)
88
- public_items = [CompanionPublic(**c.model_dump()) for c in items]
128
+ public_items = [to_companion_public(c) for c in items]
89
129
  return CompanionPaginatedResponse(data=public_items, total=total)
90
130
 
91
131
  def _register_public_create_route(self) -> None:
@@ -93,30 +133,33 @@ class CompanionRouter(
93
133
 
94
134
  @self.router.post(
95
135
  f"/{self.resource_name}",
96
- response_model=CompanionSingleItemResponse,
136
+ response_model=CompanionPublic,
97
137
  status_code=status.HTTP_201_CREATED,
98
138
  summary="Create Companion",
99
- description="Create a new Companion (public endpoint)",
139
+ description="Create a new Companion. Returns the created companion data.",
100
140
  )
101
141
  async def create_companion(
102
142
  request: CompanionCreate,
103
143
  repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
104
144
  ):
105
145
  """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)
146
+ # Exclude system_prompt from request - should not be set via API
147
+ sanitized_data = request.model_dump(exclude={"system_prompt"})
148
+ sanitized_request = CompanionCreate(**sanitized_data)
149
+
150
+ created = repository.create(sanitized_request)
151
+ # Return created companion as public schema
152
+ return to_companion_public(created)
110
153
 
111
154
  def _register_public_get_route(self) -> None:
112
155
  """GET /{resource_name}/{id} - Get a Companion (public)"""
113
156
 
114
157
  @self.router.get(
115
158
  f"/{self.resource_name}/{{item_id}}",
116
- response_model=CompanionSingleItemResponse,
159
+ response_model=CompanionPublic,
117
160
  status_code=status.HTTP_200_OK,
118
161
  summary="Get Companion",
119
- description="Get Companion by ID (public endpoint)",
162
+ description="Get Companion by ID (public endpoint). Returns companion data directly.",
120
163
  responses={
121
164
  404: {"model": ErrorResponse, "description": "Companion not found"},
122
165
  },
@@ -127,14 +170,85 @@ class CompanionRouter(
127
170
  ):
128
171
  """Get Companion by ID."""
129
172
  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)
173
+ # Return companion as public schema
174
+ return to_companion_public(companion)
175
+
176
+ def _register_public_update_route(self) -> None:
177
+ """PATCH /{resource_name}/{id} - Update Companion (public)"""
178
+
179
+ @self.router.patch(
180
+ f"/{self.resource_name}/{{item_id}}",
181
+ response_model=CompanionPublic,
182
+ status_code=status.HTTP_200_OK,
183
+ summary="Update Companion",
184
+ description="Update Companion by ID (public endpoint). Returns updated companion data directly.",
185
+ responses={
186
+ 404: {"model": ErrorResponse, "description": "Companion not found"},
187
+ },
188
+ )
189
+ async def update_companion(
190
+ item_id: str,
191
+ data: CompanionUpdate,
192
+ repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
193
+ ):
194
+ """Update Companion."""
195
+ # Exclude system_prompt from request - should not be set via API
196
+ sanitized_data = data.model_dump(
197
+ exclude={"system_prompt"}, exclude_unset=True
198
+ )
199
+ sanitized_request = CompanionUpdate(**sanitized_data)
200
+
201
+ updated = repository.update(item_id, sanitized_request)
202
+ if not updated:
203
+ raise HTTPException(
204
+ status_code=status.HTTP_404_NOT_FOUND,
205
+ detail="Companion not found",
206
+ )
207
+
208
+ # Return updated companion as public schema
209
+ return to_companion_public(updated)
210
+
211
+ def _register_public_delete_route(self) -> None:
212
+ """DELETE /{resource_name}/{id} - Remove Companion from Group (public)"""
213
+
214
+ @self.router.delete(
215
+ f"/{self.resource_name}/{{item_id}}",
216
+ response_model=CompanionPublic,
217
+ status_code=status.HTTP_200_OK,
218
+ summary="Remove Companion from Group",
219
+ description="Remove Companion from AIdol group (unassign aidol_id). Does not delete the record.",
220
+ responses={
221
+ 404: {"model": ErrorResponse, "description": "Companion not found"},
222
+ },
223
+ )
224
+ async def delete_companion(
225
+ item_id: str,
226
+ repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
227
+ ):
228
+ """Remove Companion from Group (Unassign)."""
229
+ # Get item first
230
+ self._get_item_or_404(repository, item_id)
231
+
232
+ # Update aidol_id to None (remove from group)
233
+ update_data = CompanionUpdate(aidol_id=None)
234
+
235
+ updated = repository.update(item_id, update_data)
236
+
237
+ if not updated:
238
+ raise HTTPException(
239
+ status_code=status.HTTP_404_NOT_FOUND,
240
+ detail="Companion not found",
241
+ )
242
+
243
+ # Return updated companion as public schema
244
+ return to_companion_public(updated)
133
245
 
134
246
 
135
247
  def create_companion_router(
248
+ google_settings: GoogleGenAISettings | None,
136
249
  db_session_factory: sessionmaker,
137
250
  repository_factory: CompanionRepositoryFactoryProtocol,
251
+ image_storage: ImageStorageProtocol,
138
252
  jwt_settings: JWTSettings | None = None,
139
253
  user_info_provider: UserInfoProvider | None = None,
140
254
  resource_name: str = "companions",
@@ -144,8 +258,10 @@ def create_companion_router(
144
258
  Create Companion router with dependency injection.
145
259
 
146
260
  Args:
261
+ google_settings: Google API settings (uses ADC if api_key is None)
147
262
  db_session_factory: Database session factory
148
263
  repository_factory: Factory implementing CompanionRepositoryFactoryProtocol
264
+ image_storage: Image storage for permanent URLs
149
265
  jwt_settings: Optional JWT settings for authentication
150
266
  user_info_provider: Optional user info provider
151
267
  resource_name: Resource name for routes (default: "companions")
@@ -155,6 +271,8 @@ def create_companion_router(
155
271
  FastAPI APIRouter instance
156
272
  """
157
273
  router = CompanionRouter(
274
+ google_settings=google_settings,
275
+ image_storage=image_storage,
158
276
  model_class=Companion,
159
277
  create_schema=CompanionCreate,
160
278
  update_schema=CompanionUpdate,
aidol/api/lead.py ADDED
@@ -0,0 +1,123 @@
1
+ """
2
+ Lead API router
3
+
4
+ Public endpoints for collecting leads (emails).
5
+ """
6
+
7
+ from typing import Annotated
8
+
9
+ from aioia_core.fastapi import BaseCrudRouter
10
+ from aioia_core.settings import JWTSettings
11
+ from fastapi import APIRouter, Depends, Header, status
12
+ from pydantic import BaseModel
13
+ from sqlalchemy.orm import Session, sessionmaker
14
+
15
+ from aidol.protocols import (
16
+ AIdolLeadRepositoryFactoryProtocol,
17
+ AIdolLeadRepositoryProtocol,
18
+ AIdolRepositoryFactoryProtocol,
19
+ NoUpdate,
20
+ )
21
+ from aidol.schemas import AIdolLead, AIdolLeadCreate, AIdolUpdate
22
+
23
+
24
+ class LeadResponse(BaseModel):
25
+ """Response for lead creation."""
26
+
27
+ email: str
28
+
29
+
30
+ class LeadRouter(
31
+ BaseCrudRouter[AIdolLead, AIdolLeadCreate, NoUpdate, AIdolLeadRepositoryProtocol]
32
+ ):
33
+ """
34
+ Lead router.
35
+
36
+ Handles email collection.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ aidol_repository_factory: AIdolRepositoryFactoryProtocol,
42
+ **kwargs,
43
+ ):
44
+ super().__init__(**kwargs)
45
+ self.aidol_repository_factory = aidol_repository_factory
46
+
47
+ def _register_routes(self) -> None:
48
+ """Register routes."""
49
+ self._register_create_lead_route()
50
+
51
+ def _register_create_lead_route(self) -> None:
52
+ """POST /leads - Collect email"""
53
+
54
+ @self.router.post(
55
+ f"/{self.resource_name}",
56
+ response_model=LeadResponse,
57
+ status_code=status.HTTP_201_CREATED,
58
+ summary="Collect Lead",
59
+ description="Collect email. Associates with AIdol if ClaimToken is valid.",
60
+ )
61
+ async def create_lead(
62
+ request: AIdolLeadCreate,
63
+ claim_token: Annotated[str | None, Header(alias="ClaimToken")] = None,
64
+ db_session: Session = Depends(self.get_db_dep),
65
+ lead_repository: AIdolLeadRepositoryProtocol = Depends(
66
+ self.get_repository_dep
67
+ ),
68
+ ):
69
+ """Collect email."""
70
+ email_saved = False
71
+
72
+ # 1. Try to associate with AIdol if token is present
73
+ if claim_token:
74
+ # Reuse session from dependency
75
+ aidol_repo = self.aidol_repository_factory.create_repository(db_session)
76
+
77
+ # Find AIdol by claim_token
78
+ # Assuming get_all supports filters
79
+ items, _ = aidol_repo.get_all(
80
+ filters=[
81
+ {
82
+ "field": "claim_token",
83
+ "operator": "eq",
84
+ "value": claim_token,
85
+ }
86
+ ]
87
+ )
88
+
89
+ if items:
90
+ aidol = items[0]
91
+ # Update AIdol email
92
+ aidol_repo.update(aidol.id, AIdolUpdate(email=request.email))
93
+ email_saved = True
94
+
95
+ # 2. If not saved as AIdol email, create Lead
96
+ if not email_saved:
97
+ lead_repository.create(request)
98
+
99
+ return LeadResponse(email=request.email)
100
+
101
+
102
+ def create_lead_router(
103
+ db_session_factory: sessionmaker,
104
+ aidol_repository_factory: AIdolRepositoryFactoryProtocol,
105
+ lead_repository_factory: AIdolLeadRepositoryFactoryProtocol,
106
+ jwt_settings: JWTSettings | None = None,
107
+ resource_name: str = "leads",
108
+ tags: list[str] | None = None,
109
+ ) -> APIRouter:
110
+ """Create Lead router."""
111
+ router = LeadRouter(
112
+ model_class=AIdolLead,
113
+ create_schema=AIdolLeadCreate,
114
+ update_schema=NoUpdate, # Update not supported
115
+ db_session_factory=db_session_factory,
116
+ repository_factory=lead_repository_factory,
117
+ aidol_repository_factory=aidol_repository_factory,
118
+ user_info_provider=None, # No auth required for lead collection
119
+ jwt_secret_key=jwt_settings.secret_key if jwt_settings else None,
120
+ resource_name=resource_name,
121
+ tags=tags or ["Lead"],
122
+ )
123
+ return router.get_router()
aidol/factories.py CHANGED
@@ -7,6 +7,7 @@ Uses BaseRepositoryFactory for BaseCrudRouter compatibility.
7
7
  from aioia_core.factories import BaseRepositoryFactory
8
8
 
9
9
  from aidol.repositories.aidol import AIdolRepository
10
+ from aidol.repositories.aidol_lead import AIdolLeadRepository
10
11
  from aidol.repositories.companion import CompanionRepository
11
12
 
12
13
 
@@ -22,3 +23,10 @@ class CompanionRepositoryFactory(BaseRepositoryFactory[CompanionRepository]):
22
23
 
23
24
  def __init__(self):
24
25
  super().__init__(repository_class=CompanionRepository)
26
+
27
+
28
+ class AIdolLeadRepositoryFactory(BaseRepositoryFactory[AIdolLeadRepository]):
29
+ """Factory for creating AIdolLead repositories."""
30
+
31
+ def __init__(self):
32
+ super().__init__(repository_class=AIdolLeadRepository)
aidol/protocols.py CHANGED
@@ -11,11 +11,14 @@ from typing import Protocol
11
11
 
12
12
  import PIL.Image
13
13
  from aioia_core import CrudRepositoryProtocol
14
+ from pydantic import BaseModel
14
15
  from sqlalchemy.orm import Session
15
16
 
16
17
  from aidol.schemas import (
17
18
  AIdol,
18
19
  AIdolCreate,
20
+ AIdolLead,
21
+ AIdolLeadCreate,
19
22
  AIdolUpdate,
20
23
  Companion,
21
24
  CompanionCreate,
@@ -23,6 +26,10 @@ from aidol.schemas import (
23
26
  )
24
27
 
25
28
 
29
+ class NoUpdate(BaseModel):
30
+ """Placeholder for repositories without update support."""
31
+
32
+
26
33
  class AIdolRepositoryProtocol(
27
34
  CrudRepositoryProtocol[AIdol, AIdolCreate, AIdolUpdate], Protocol
28
35
  ):
@@ -98,3 +105,22 @@ class ImageStorageProtocol(Protocol):
98
105
  image: PIL Image object to upload.
99
106
  """
100
107
  ...
108
+
109
+
110
+ class AIdolLeadRepositoryProtocol(
111
+ CrudRepositoryProtocol[AIdolLead, AIdolLeadCreate, NoUpdate], Protocol
112
+ ):
113
+ """Protocol defining AIdolLead repository expectations.
114
+
115
+ Inherits CRUD operations from CrudRepositoryProtocol.
116
+ """
117
+
118
+
119
+ class AIdolLeadRepositoryFactoryProtocol(Protocol):
120
+ """Protocol for factory that creates AIdolLeadRepositoryProtocol instances."""
121
+
122
+ def create_repository(
123
+ self, db_session: Session | None = None
124
+ ) -> AIdolLeadRepositoryProtocol:
125
+ """Create a repository instance."""
126
+ ...
@@ -7,17 +7,13 @@ Implements BaseRepository pattern for BaseCrudRouter compatibility.
7
7
  from datetime import timezone
8
8
 
9
9
  from aioia_core.repositories import BaseRepository
10
- from pydantic import BaseModel
11
10
  from sqlalchemy.orm import Session
12
11
 
13
12
  from aidol.models import DBAIdolLead
13
+ from aidol.protocols import AIdolLeadRepositoryProtocol, NoUpdate
14
14
  from aidol.schemas import AIdolLead, AIdolLeadCreate
15
15
 
16
16
 
17
- class _AIdolLeadUpdate(BaseModel):
18
- """Placeholder for update schema (not used)."""
19
-
20
-
21
17
  def _convert_db_aidol_lead_to_model(db_lead: DBAIdolLead) -> AIdolLead:
22
18
  """Convert DB AIdolLead to Pydantic model."""
23
19
  return AIdolLead(
@@ -35,7 +31,8 @@ def _convert_aidol_lead_create_to_db(schema: AIdolLeadCreate) -> dict:
35
31
 
36
32
 
37
33
  class AIdolLeadRepository(
38
- BaseRepository[AIdolLead, DBAIdolLead, AIdolLeadCreate, _AIdolLeadUpdate]
34
+ BaseRepository[AIdolLead, DBAIdolLead, AIdolLeadCreate, NoUpdate],
35
+ AIdolLeadRepositoryProtocol,
39
36
  ):
40
37
  """
41
38
  Database-backed AIdolLead repository.
@@ -0,0 +1,96 @@
1
+ """
2
+ Companion service
3
+
4
+ Business logic for Companion operations including grade and MBTI calculation.
5
+ """
6
+
7
+ from aidol.schemas.companion import Companion, CompanionPublic, CompanionStats, Grade
8
+
9
+
10
+ def calculate_grade(stats: CompanionStats) -> Grade:
11
+ """Calculate grade based on stats average.
12
+
13
+ - A: 80-100
14
+ - B: 60-79
15
+ - C: 40-59
16
+ - F: 0-39
17
+ """
18
+ avg = (
19
+ (stats.vocal or 0)
20
+ + (stats.dance or 0)
21
+ + (stats.rap or 0)
22
+ + (stats.visual or 0)
23
+ + (stats.stamina or 0)
24
+ + (stats.charm or 0)
25
+ ) / 6
26
+ if avg >= 80:
27
+ return Grade.A
28
+ if avg >= 60:
29
+ return Grade.B
30
+ if avg >= 40:
31
+ return Grade.C
32
+ return Grade.F
33
+
34
+
35
+ def calculate_mbti(
36
+ energy: int | None,
37
+ perception: int | None,
38
+ judgment: int | None,
39
+ lifestyle: int | None,
40
+ ) -> str | None:
41
+ """Calculate MBTI string from 4 dimension scores.
42
+
43
+ Each score is 1-10:
44
+ - energy: 1-5 = E, 6-10 = I
45
+ - perception: 1-5 = N, 6-10 = S
46
+ - judgment: 1-5 = T, 6-10 = F
47
+ - lifestyle: 1-5 = P, 6-10 = J
48
+
49
+ Returns None if any dimension is missing.
50
+ """
51
+ if any(v is None for v in (energy, perception, judgment, lifestyle)):
52
+ return None
53
+
54
+ assert energy is not None
55
+ assert perception is not None
56
+ assert judgment is not None
57
+ assert lifestyle is not None
58
+
59
+ e_i = "E" if energy <= 5 else "I"
60
+ n_s = "N" if perception <= 5 else "S"
61
+ t_f = "T" if judgment <= 5 else "F"
62
+ p_j = "P" if lifestyle <= 5 else "J"
63
+
64
+ return f"{e_i}{n_s}{t_f}{p_j}"
65
+
66
+
67
+ def to_companion_public(companion: Companion) -> CompanionPublic:
68
+ """Convert Companion to CompanionPublic with calculated grade and mbti."""
69
+ # Build stats object
70
+ stats = companion.stats if companion.stats else CompanionStats()
71
+
72
+ # Calculate grade from stats
73
+ grade = calculate_grade(stats)
74
+
75
+ # Calculate MBTI from 4 dimensions
76
+ mbti = calculate_mbti(
77
+ companion.mbti_energy,
78
+ companion.mbti_perception,
79
+ companion.mbti_judgment,
80
+ companion.mbti_lifestyle,
81
+ )
82
+
83
+ return CompanionPublic(
84
+ id=companion.id,
85
+ aidol_id=companion.aidol_id,
86
+ name=companion.name,
87
+ gender=companion.gender,
88
+ grade=grade,
89
+ biography=companion.biography,
90
+ profile_picture_url=companion.profile_picture_url,
91
+ position=companion.position,
92
+ mbti=mbti,
93
+ stats=stats,
94
+ created_at=companion.created_at,
95
+ updated_at=companion.updated_at,
96
+ )
@@ -1,145 +1,112 @@
1
1
  """
2
- Image generation service for AIdol
2
+ Image generation service for AIdol.
3
3
 
4
- Generates images using OpenAI DALL-E 3 for AIdol emblems and Companion profiles.
4
+ Generates images using Google Gemini for AIdol emblems and Companion profiles.
5
5
  """
6
6
 
7
+ from __future__ import annotations
8
+
7
9
  import logging
8
10
  from dataclasses import dataclass
9
11
  from io import BytesIO
10
12
  from typing import Literal
11
13
 
12
- import httpx
13
- import openai
14
14
  import PIL.Image
15
- from aioia_core.settings import OpenAIAPISettings
15
+ from google import genai
16
+ from google.genai import errors as genai_errors
17
+
18
+ from aidol.settings import GoogleGenAISettings
16
19
 
17
20
  logger = logging.getLogger(__name__)
18
21
 
19
22
 
20
23
  @dataclass
21
24
  class ImageGenerationResponse:
22
- """Structured response from the Image Generation service"""
25
+ """Structured response for compatibility (legacy)"""
23
26
 
24
- url: str
25
- revised_prompt: str | None
27
+ url: str | None = None
28
+ revised_prompt: str | None = None
26
29
 
27
30
 
28
31
  class ImageGenerationService:
29
- """Service for generating images using OpenAI DALL-E 3"""
32
+ """Service for generating images using Google Gemini 3 (Imagen)."""
30
33
 
31
- def __init__(self, openai_settings: OpenAIAPISettings):
32
- """
33
- Initialize the Image Generation service with OpenAI settings.
34
+ client: "genai.Client | None" = None
34
35
 
35
- Args:
36
- openai_settings: OpenAI settings containing required API key
36
+ def __init__(self, settings: GoogleGenAISettings | None = None):
37
37
  """
38
- self.settings = openai_settings
39
- self.client = openai.OpenAI(api_key=self.settings.api_key)
38
+ Initialize the Image Generation service.
40
39
 
41
- def generate_image(
42
- self,
43
- prompt: str,
44
- size: Literal[
45
- "1024x1024",
46
- "1792x1024",
47
- "1024x1792",
48
- ] = "1024x1024",
49
- quality: Literal["standard", "hd"] = "standard",
50
- ) -> ImageGenerationResponse | None:
51
- """
52
- Generate an image from a text prompt using OpenAI DALL-E 3.
40
+ Supports two authentication methods:
41
+ 1. Google AI API: settings.api_key
42
+ 2. Vertex AI API (ADC): settings.cloud_project (location=global hardcoded)
53
43
 
54
44
  Args:
55
- prompt: Text description of the image to generate
56
- size: Image size (default: "1024x1024")
57
- quality: Image quality "standard" or "hd" (default: "standard")
58
-
59
- Returns:
60
- An ImageGenerationResponse object containing the image URL and revised prompt,
61
- or None if generation fails.
62
-
63
- Raises:
64
- openai.OpenAIError: If OpenAI API call fails.
45
+ settings: GoogleGenAISettings for configuration.
65
46
  """
66
- try:
67
- logger.info("Generating image with OpenAI DALL-E 3...")
68
- response = self.client.images.generate(
69
- model="dall-e-3",
70
- prompt=prompt,
71
- size=size,
72
- quality=quality,
73
- n=1,
47
+ # Priority 1: Settings with api_key (Google AI API)
48
+ if settings and settings.api_key:
49
+ self.client = genai.Client(api_key=settings.api_key)
50
+ # Priority 2: Settings with cloud_project (Vertex AI, location=global)
51
+ elif settings and settings.cloud_project:
52
+ self.client = genai.Client(
53
+ vertexai=True,
54
+ project=settings.cloud_project,
55
+ location="global",
74
56
  )
75
-
76
- if not response.data or len(response.data) == 0:
77
- logger.error("No image data returned from OpenAI")
78
- return None
79
-
80
- image_data = response.data[0]
81
- url = image_data.url
82
-
83
- if not url:
84
- logger.error("No URL found in image response")
85
- return None
86
-
87
- revised_prompt = image_data.revised_prompt
88
-
89
- logger.info("Successfully generated image: %s", url[:100])
90
- return ImageGenerationResponse(
91
- url=url,
92
- revised_prompt=revised_prompt,
57
+ else:
58
+ logger.error(
59
+ "No authentication configured. "
60
+ "Set GOOGLE_API_KEY or GOOGLE_CLOUD_PROJECT"
93
61
  )
94
-
95
- except openai.OpenAIError as e:
96
- logger.error("OpenAI API error: %s", e)
97
- raise
98
-
99
- def _download_image(self, url: str) -> PIL.Image.Image:
100
- """Download image from URL and return as PIL Image.
101
-
102
- Args:
103
- url: URL of the image to download.
104
-
105
- Raises:
106
- httpx.HTTPError: If download fails.
107
- """
108
- with httpx.Client(timeout=30.0) as client:
109
- response = client.get(url)
110
- response.raise_for_status()
111
- return PIL.Image.open(BytesIO(response.content))
62
+ self.client = None
112
63
 
113
64
  def generate_and_download_image(
114
65
  self,
115
66
  prompt: str,
116
- size: Literal[
117
- "1024x1024",
118
- "1792x1024",
119
- "1024x1792",
120
- ] = "1024x1024",
121
- quality: Literal["standard", "hd"] = "standard",
67
+ size: Literal["1024x1024"] = "1024x1024", # pylint: disable=unused-argument
68
+ quality: Literal["standard"] = "standard", # pylint: disable=unused-argument
122
69
  ) -> PIL.Image.Image | None:
123
- """Generate an image and download as PIL Image.
124
-
125
- DALL-E returns temporary URLs that expire in 1-2 hours.
126
- Use this method to download the image immediately after generation.
70
+ """
71
+ Generate an image using Gemini 3 and return as PIL Image.
127
72
 
128
73
  Args:
129
- prompt: Text description of the image to generate.
130
- size: Image size (default: "1024x1024").
131
- quality: Image quality "standard" or "hd" (default: "standard").
74
+ prompt: Text description.
75
+ size: Ignored (Gemini specific).
76
+ quality: Ignored (Gemini specific).
132
77
 
133
78
  Returns:
134
79
  PIL Image object, or None if generation fails.
135
-
136
- Raises:
137
- openai.OpenAIError: If OpenAI API call fails.
138
- httpx.HTTPError: If image download fails.
139
80
  """
140
- result = self.generate_image(prompt, size, quality)
141
- if result is None:
81
+ if not self.client:
82
+ logger.error("Gemini client not initialized")
83
+ return None
84
+
85
+ try:
86
+ logger.info("Generating image with Gemini 3 (prompt: %s)...", prompt[:50])
87
+
88
+ response = self.client.models.generate_content(
89
+ model="gemini-3-pro-image-preview",
90
+ contents=[prompt], # type: ignore[arg-type]
91
+ )
92
+
93
+ # Iterate parts to find the image
94
+ if response.parts:
95
+ for part in response.parts:
96
+ if part.inline_data and part.inline_data.data:
97
+ logger.info("Successfully generated image via Gemini.")
98
+ # Manually convert bytes to PIL Image to ensure it's a standard PIL object
99
+ # compatible with main.py's save(format="PNG") call.
100
+ return PIL.Image.open(BytesIO(part.inline_data.data))
101
+
102
+ logger.warning("No image data found in Gemini response.")
103
+ return None
104
+
105
+ except genai_errors.APIError as e:
106
+ logger.error("Gemini API error: code=%s, message=%s", e.code, e.message)
142
107
  return None
143
108
 
144
- logger.info("Downloading image from DALL-E temporary URL...")
145
- return self._download_image(result.url)
109
+ # Legacy methods for compatibility if needed (can be removed or shimmed)
110
+ def generate_image(self, *args, **kwargs): # pylint: disable=unused-argument
111
+ """Deprecated: Use generate_and_download_image instead."""
112
+ logger.warning("generate_image is deprecated for Gemini service.")
aidol/settings.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ Environment settings for aidol module.
3
+
4
+ Provides GoogleGenAISettings for image generation with Google Gemini.
5
+ """
6
+
7
+ from pydantic_settings import BaseSettings
8
+
9
+
10
+ class GoogleGenAISettings(BaseSettings):
11
+ """
12
+ Google Gen AI SDK settings for Gemini image generation.
13
+
14
+ Supports two authentication methods:
15
+ 1. Google AI API: GOOGLE_API_KEY
16
+ 2. Vertex AI API (ADC): GOOGLE_CLOUD_PROJECT
17
+
18
+ For Google AI API:
19
+ export GOOGLE_API_KEY=your-api-key
20
+
21
+ For Vertex AI with ADC:
22
+ export GOOGLE_CLOUD_PROJECT=your-project-id
23
+ gcloud auth application-default login
24
+
25
+ Note: Vertex AI uses location="global" (hardcoded) because
26
+ Gemini image generation models only support the global endpoint.
27
+
28
+ Environment variables:
29
+ GOOGLE_API_KEY: Google API key (optional)
30
+ GOOGLE_CLOUD_PROJECT: GCP project ID for Vertex AI (optional)
31
+ """
32
+
33
+ api_key: str | None = None
34
+ cloud_project: str | None = None
35
+
36
+ class Config:
37
+ env_prefix = "GOOGLE_"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-aidol
3
- Version: 0.2.0
3
+ Version: 0.4.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)
@@ -33,7 +34,7 @@ AI 아이돌 그룹 생성 및 채팅 Python 패키지
33
34
  ## 주요 기능
34
35
 
35
36
  - AI 아이돌 그룹/멤버 CRUD
36
- - DALL-E 3 이미지 생성 (엠블럼, 프로필)
37
+ - Google Gemini 이미지 생성 (엠블럼, 프로필)
37
38
  - 텍스트 채팅 (페르소나 기반 응답)
38
39
  - Buppy 통합 Adapter 패턴
39
40
 
@@ -61,7 +62,7 @@ from aidol.factories import AIdolRepositoryFactory, CompanionRepositoryFactory
61
62
  # AIdol 라우터
62
63
  aidol_router = AIdolRouter(
63
64
  repository_factory=AIdolRepositoryFactory(),
64
- openai_settings=openai_settings,
65
+ google_settings=google_settings,
65
66
  image_storage=image_storage,
66
67
  )
67
68
 
@@ -86,11 +87,27 @@ make format
86
87
 
87
88
  ## 환경 변수
88
89
 
89
- ### 필수 (이미지 생성 )
90
+ ### 이미지 생성 인증 (선택, ADC 지원)
90
91
 
91
92
  | 변수 | 설명 |
92
93
  |------|------|
93
- | `OPENAI_API_KEY` | OpenAI API 키 |
94
+ | `GOOGLE_API_KEY` | Google API 키 (Google AI API) |
95
+ | `GOOGLE_CLOUD_PROJECT` | GCP 프로젝트 ID (Vertex AI) |
96
+
97
+ **인증 방법:**
98
+
99
+ **Option 1: Google AI API (API Key)**
100
+ ```bash
101
+ export GOOGLE_API_KEY=your-api-key
102
+ ```
103
+
104
+ **Option 2: Vertex AI (ADC)**
105
+ ```bash
106
+ export GOOGLE_CLOUD_PROJECT=your-project-id
107
+ gcloud auth application-default login # 로컬 개발
108
+ ```
109
+
110
+ > **참고**: Vertex AI 사용 시 `location=global`이 하드코딩되어 있습니다 (Gemini 이미지 생성 모델 요구사항).
94
111
 
95
112
  ### 선택
96
113
 
@@ -105,7 +122,8 @@ make format
105
122
 
106
123
  - aioia-core (공통 인프라)
107
124
  - FastAPI, SQLAlchemy, Pydantic
108
- - OpenAI (이미지 생성, 채팅)
125
+ - Google Generative AI (이미지 생성)
126
+ - OpenAI (채팅)
109
127
  - Pillow (이미지 처리)
110
128
 
111
129
  ## 라이선스
@@ -1,24 +1,28 @@
1
1
  aidol/__init__.py,sha256=iMN-aij1k6vElJt0sZQT4QYJjvoD27Q9vtZkQA0TY9c,141
2
2
  aidol/api/__init__.py,sha256=skD_w82nT0v1hdKK9BBOycNERexIr8F1BmSmSono4Jk,276
3
- aidol/api/aidol.py,sha256=DQzwF41bnfw8JpcrZlV3XDkvuH5AszyYCJwOmcRxTKg,6684
4
- aidol/api/companion.py,sha256=QBsPgSjBp1USunB6Jth6fdWLwBzsI6KIq6XQZu9OifQ,6287
5
- aidol/factories.py,sha256=HYkJN9qu1fZOgLW6hk3t0Ixh41mVB4tzzmOQQcso9tA,698
3
+ aidol/api/aidol.py,sha256=AIpDMTTOxht1eEVk4YITvmJzvklpYlDTpQVrIuSjeUw,6588
4
+ aidol/api/common.py,sha256=w8ERo96t9tat4HPs1qw9JhyoJ8cj5crHCe92Hp3Usug,2198
5
+ aidol/api/companion.py,sha256=Nlx4QHqoi0y_CMczEh75n8C4xJTWp8eV1BqHjEzkOFw,10840
6
+ aidol/api/lead.py,sha256=RSf3GcIUVJu752rU9HG7Wy22UmnrRZnN_NGWkpTRDfE,3921
7
+ aidol/factories.py,sha256=5VhEbUVQEo-WruOdDauOi9xGgMxrgT339glocC1ua4o,983
6
8
  aidol/models/__init__.py,sha256=AljQMgSE9vHx203NFQZMknKpzHIfyFLLcOMnFpMOLAs,218
7
9
  aidol/models/aidol.py,sha256=By82BqiAasLNy8ZCNON2m46BnSCfL2J_ZFLO6_MMFO0,903
8
10
  aidol/models/aidol_lead.py,sha256=xCa1AqJdBBeW2Edcj1pK-cXbX5oatxzCOkPyqEGBXVw,619
9
11
  aidol/models/companion.py,sha256=fom58GXjGvAxxndS4X4MrT1HWNw8Ps99BNEyPd5JhW0,1974
10
- aidol/protocols.py,sha256=Zc9LWUcdHJlerI649v3maXbgXARiXrBiRd2cxNLnSDE,2667
12
+ aidol/protocols.py,sha256=8-7iwbtMv5vQUAYGRbbKvm6YfYZZ1VZz6dufse_DYp4,3375
11
13
  aidol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
14
  aidol/repositories/__init__.py,sha256=4kE68a-eRhXt9z5a2XCn1_2s-p3VMpQIdL3oRDb1Ns0,296
13
15
  aidol/repositories/aidol.py,sha256=-hf7WW_sDx4vE9bHzKQnzfkgu-on7sqOem9gFO8x0EE,1810
14
- aidol/repositories/aidol_lead.py,sha256=g15VJwOI-w_a1fe0OzSUUC0zdfqKSR2JHqywdcOvTfU,1516
16
+ aidol/repositories/aidol_lead.py,sha256=vmCHXdTtxDSiRpd_n7DuyecZDPLls56r0N5vyT_v9zI,1488
15
17
  aidol/repositories/companion.py,sha256=dUkJA0me2kmxqk3B4L0w4ENcHeAQFw5ki6RvZ5eLHDg,2877
16
18
  aidol/schemas/__init__.py,sha256=sNurP-s24PgS4ZJ7xusZ7Z7wXtl1rdsnAxxdeubRXHE,923
17
19
  aidol/schemas/aidol.py,sha256=wOfHaLu4I56elLLj6A3CGriPZE4Pz4fFAyC3emtvaCE,4135
18
20
  aidol/schemas/aidol_lead.py,sha256=JS8U-ep0Ga6x0PdwXhJfTrcOCKgG0wfFW8pN5X35GUM,1070
19
21
  aidol/schemas/companion.py,sha256=I4hi4LT-S9AC7lqt1jyYfd0vSqYmxYNm2k9EsZdyNyM,7584
20
22
  aidol/services/__init__.py,sha256=3vdT_CtUfeDWbsPn7Xnp41sajovcl2nCvpZ8KNFPHYM,144
21
- aidol/services/image_generation_service.py,sha256=naqOxe5jsSTs9__Nj3gwBtOQPkfWvgqVV4z6XkA6DGM,4359
22
- py_aidol-0.2.0.dist-info/METADATA,sha256=oVVlTXIUXp2BPT61cQnQLav5lozJwul0H0SOOWJdrpc,2880
23
- py_aidol-0.2.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
24
- py_aidol-0.2.0.dist-info/RECORD,,
23
+ aidol/services/companion_service.py,sha256=tNNWiIFmJQ-I3UBW06baOANXhBx5oTKoT6nkqfnDisA,2490
24
+ aidol/services/image_generation_service.py,sha256=zDs9HP_JckHFJJZUIo2ADSsHGppat-8V-ttu8DlN-BU,3862
25
+ aidol/settings.py,sha256=7oI3Vn1iGXvLRRahJ1ygD6qIu-BvZmlVvvMQxnsq1kc,1003
26
+ py_aidol-0.4.0.dist-info/METADATA,sha256=FI4-Xdaxi36hwykQQGgmttCOlsFOlSh_NzCluamZIvA,3431
27
+ py_aidol-0.4.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
28
+ py_aidol-0.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.3.0
2
+ Generator: poetry-core 2.3.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any