py-aidol 0.1.0__tar.gz

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