py-aidol 0.1.0__tar.gz → 0.2.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.
Files changed (26) hide show
  1. {py_aidol-0.1.0 → py_aidol-0.2.0}/PKG-INFO +1 -1
  2. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/models/__init__.py +2 -1
  3. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/models/aidol.py +4 -2
  4. py_aidol-0.2.0/aidol/models/aidol_lead.py +22 -0
  5. py_aidol-0.2.0/aidol/models/companion.py +45 -0
  6. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/repositories/__init__.py +2 -0
  7. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/repositories/aidol.py +2 -0
  8. py_aidol-0.2.0/aidol/repositories/aidol_lead.py +52 -0
  9. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/repositories/companion.py +38 -5
  10. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/schemas/__init__.py +12 -0
  11. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/schemas/aidol.py +10 -6
  12. py_aidol-0.2.0/aidol/schemas/aidol_lead.py +38 -0
  13. py_aidol-0.2.0/aidol/schemas/companion.py +210 -0
  14. {py_aidol-0.1.0 → py_aidol-0.2.0}/pyproject.toml +1 -1
  15. py_aidol-0.1.0/aidol/models/companion.py +0 -27
  16. py_aidol-0.1.0/aidol/schemas/companion.py +0 -93
  17. {py_aidol-0.1.0 → py_aidol-0.2.0}/README.md +0 -0
  18. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/__init__.py +0 -0
  19. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/api/__init__.py +0 -0
  20. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/api/aidol.py +0 -0
  21. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/api/companion.py +0 -0
  22. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/factories.py +0 -0
  23. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/protocols.py +0 -0
  24. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/py.typed +0 -0
  25. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/services/__init__.py +0 -0
  26. {py_aidol-0.1.0 → py_aidol-0.2.0}/aidol/services/image_generation_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-aidol
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -3,6 +3,7 @@ AIdol database models
3
3
  """
4
4
 
5
5
  from aidol.models.aidol import DBAIdol
6
+ from aidol.models.aidol_lead import DBAIdolLead
6
7
  from aidol.models.companion import DBCompanion
7
8
 
8
- __all__ = ["DBAIdol", "DBCompanion"]
9
+ __all__ = ["DBAIdol", "DBAIdolLead", "DBCompanion"]
@@ -18,7 +18,9 @@ class DBAIdol(BaseModel):
18
18
  __tablename__ = "aidols"
19
19
 
20
20
  # id, created_at, updated_at inherited from BaseModel
21
- name: Mapped[str] = mapped_column(String, nullable=False)
21
+ name: Mapped[str | None] = mapped_column(String, nullable=True)
22
22
  concept: Mapped[str | None] = mapped_column(String, nullable=True)
23
- profile_image_url: Mapped[str] = mapped_column(String, nullable=False)
23
+ profile_image_url: Mapped[str | None] = mapped_column(String, nullable=True)
24
24
  claim_token: Mapped[str | None] = mapped_column(String(36), nullable=True)
25
+ email: Mapped[str | None] = mapped_column(String, nullable=True)
26
+ greeting: Mapped[str | None] = mapped_column(String, nullable=True)
@@ -0,0 +1,22 @@
1
+ """
2
+ AIdol Leads 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 DBAIdolLead(BaseModel):
16
+ """AIdol Lead (viewer email) database model"""
17
+
18
+ __tablename__ = "aidol_leads"
19
+
20
+ # id, created_at, updated_at inherited from BaseModel
21
+ aidol_id: Mapped[str] = mapped_column(String, nullable=False)
22
+ email: Mapped[str] = mapped_column(String, nullable=False)
@@ -0,0 +1,45 @@
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, Integer, 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 | None] = mapped_column(String, nullable=True)
23
+ gender: Mapped[str | None] = mapped_column(String, nullable=True)
24
+ grade: Mapped[str | None] = mapped_column(String, nullable=True)
25
+ biography: Mapped[str | None] = mapped_column(Text, nullable=True)
26
+ profile_picture_url: Mapped[str | None] = mapped_column(String, nullable=True)
27
+ system_prompt: Mapped[str | None] = mapped_column(Text, nullable=True)
28
+
29
+ # MBTI scores (1-10 scale)
30
+ mbti_energy: Mapped[int | None] = mapped_column(Integer, nullable=True)
31
+ mbti_perception: Mapped[int | None] = mapped_column(Integer, nullable=True)
32
+ mbti_judgment: Mapped[int | None] = mapped_column(Integer, nullable=True)
33
+ mbti_lifestyle: Mapped[int | None] = mapped_column(Integer, nullable=True)
34
+
35
+ # Stats (0-100 scale)
36
+ vocal: Mapped[int | None] = mapped_column(Integer, nullable=True)
37
+ dance: Mapped[int | None] = mapped_column(Integer, nullable=True)
38
+ rap: Mapped[int | None] = mapped_column(Integer, nullable=True)
39
+ visual: Mapped[int | None] = mapped_column(Integer, nullable=True)
40
+ stamina: Mapped[int | None] = mapped_column(Integer, nullable=True)
41
+ charm: Mapped[int | None] = mapped_column(Integer, nullable=True)
42
+
43
+ position: Mapped[str | None] = mapped_column(String, nullable=True)
44
+
45
+ __table_args__ = (Index("ix_companions_aidol_id", "aidol_id"),)
@@ -3,9 +3,11 @@ AIdol repositories
3
3
  """
4
4
 
5
5
  from aidol.repositories.aidol import AIdolRepository
6
+ from aidol.repositories.aidol_lead import AIdolLeadRepository
6
7
  from aidol.repositories.companion import CompanionRepository
7
8
 
8
9
  __all__ = [
9
10
  "AIdolRepository",
11
+ "AIdolLeadRepository",
10
12
  "CompanionRepository",
11
13
  ]
@@ -25,6 +25,8 @@ def _convert_db_aidol_to_model(db_aidol: DBAIdol) -> AIdol:
25
25
  return AIdol(
26
26
  id=db_aidol.id,
27
27
  name=db_aidol.name,
28
+ email=db_aidol.email,
29
+ greeting=db_aidol.greeting,
28
30
  concept=db_aidol.concept,
29
31
  profile_image_url=db_aidol.profile_image_url,
30
32
  claim_token=db_aidol.claim_token,
@@ -0,0 +1,52 @@
1
+ """
2
+ AIdol Lead repository
3
+
4
+ Implements BaseRepository pattern for BaseCrudRouter compatibility.
5
+ """
6
+
7
+ from datetime import timezone
8
+
9
+ from aioia_core.repositories import BaseRepository
10
+ from pydantic import BaseModel
11
+ from sqlalchemy.orm import Session
12
+
13
+ from aidol.models import DBAIdolLead
14
+ from aidol.schemas import AIdolLead, AIdolLeadCreate
15
+
16
+
17
+ class _AIdolLeadUpdate(BaseModel):
18
+ """Placeholder for update schema (not used)."""
19
+
20
+
21
+ def _convert_db_aidol_lead_to_model(db_lead: DBAIdolLead) -> AIdolLead:
22
+ """Convert DB AIdolLead to Pydantic model."""
23
+ return AIdolLead(
24
+ id=db_lead.id,
25
+ aidol_id=db_lead.aidol_id,
26
+ email=db_lead.email,
27
+ created_at=db_lead.created_at.replace(tzinfo=timezone.utc),
28
+ updated_at=db_lead.updated_at.replace(tzinfo=timezone.utc),
29
+ )
30
+
31
+
32
+ def _convert_aidol_lead_create_to_db(schema: AIdolLeadCreate) -> dict:
33
+ """Convert AIdolLeadCreate schema to DB model data dict."""
34
+ return schema.model_dump(exclude_unset=True)
35
+
36
+
37
+ class AIdolLeadRepository(
38
+ BaseRepository[AIdolLead, DBAIdolLead, AIdolLeadCreate, _AIdolLeadUpdate]
39
+ ):
40
+ """
41
+ Database-backed AIdolLead repository.
42
+
43
+ Extends BaseRepository for CRUD operations compatible with BaseCrudRouter.
44
+ """
45
+
46
+ def __init__(self, db_session: Session):
47
+ super().__init__(
48
+ db_session=db_session,
49
+ db_model=DBAIdolLead,
50
+ convert_to_model=_convert_db_aidol_lead_to_model,
51
+ convert_to_db_model=_convert_aidol_lead_create_to_db,
52
+ )
@@ -10,7 +10,15 @@ from aioia_core.repositories import BaseRepository
10
10
  from sqlalchemy.orm import Session
11
11
 
12
12
  from aidol.models import DBCompanion
13
- from aidol.schemas import Companion, CompanionCreate, CompanionUpdate
13
+ from aidol.schemas import (
14
+ Companion,
15
+ CompanionCreate,
16
+ CompanionStats,
17
+ CompanionUpdate,
18
+ Gender,
19
+ Grade,
20
+ Position,
21
+ )
14
22
 
15
23
 
16
24
  def _convert_db_companion_to_model(db_companion: DBCompanion) -> Companion:
@@ -23,20 +31,45 @@ def _convert_db_companion_to_model(db_companion: DBCompanion) -> Companion:
23
31
  id=db_companion.id,
24
32
  aidol_id=db_companion.aidol_id,
25
33
  name=db_companion.name,
34
+ gender=Gender(db_companion.gender) if db_companion.gender else None,
35
+ grade=Grade(db_companion.grade) if db_companion.grade else None,
26
36
  biography=db_companion.biography,
27
37
  profile_picture_url=db_companion.profile_picture_url,
38
+ position=Position(db_companion.position) if db_companion.position else None,
28
39
  system_prompt=db_companion.system_prompt,
40
+ mbti_energy=db_companion.mbti_energy,
41
+ mbti_perception=db_companion.mbti_perception,
42
+ mbti_judgment=db_companion.mbti_judgment,
43
+ mbti_lifestyle=db_companion.mbti_lifestyle,
44
+ stats=CompanionStats(
45
+ vocal=db_companion.vocal or 0,
46
+ dance=db_companion.dance or 0,
47
+ rap=db_companion.rap or 0,
48
+ visual=db_companion.visual or 0,
49
+ stamina=db_companion.stamina or 0,
50
+ charm=db_companion.charm or 0,
51
+ ),
29
52
  created_at=db_companion.created_at.replace(tzinfo=timezone.utc),
30
53
  updated_at=db_companion.updated_at.replace(tzinfo=timezone.utc),
31
54
  )
32
55
 
33
56
 
34
- def _convert_companion_create_to_db(schema: CompanionCreate) -> dict:
35
- """Convert CompanionCreate schema to DB model data dict.
57
+ def _convert_companion_schema_to_db(
58
+ schema: CompanionCreate | CompanionUpdate,
59
+ ) -> dict:
60
+ """Convert CompanionCreate/Update schema to DB model data dict.
36
61
 
62
+ Decomposes nested stats object into individual DB columns.
37
63
  Includes system_prompt for AI configuration.
38
64
  """
39
- return schema.model_dump(exclude_unset=True)
65
+ data = schema.model_dump(exclude_unset=True, exclude={"stats"})
66
+
67
+ # Decompose stats into individual columns
68
+ if schema.stats is not None:
69
+ stats_dict = schema.stats.model_dump()
70
+ data.update(stats_dict)
71
+
72
+ return data
40
73
 
41
74
 
42
75
  class CompanionRepository(
@@ -53,5 +86,5 @@ class CompanionRepository(
53
86
  db_session=db_session,
54
87
  db_model=DBCompanion,
55
88
  convert_to_model=_convert_db_companion_to_model,
56
- convert_to_db_model=_convert_companion_create_to_db,
89
+ convert_to_db_model=_convert_companion_schema_to_db,
57
90
  )
@@ -12,12 +12,17 @@ from aidol.schemas.aidol import (
12
12
  ImageGenerationRequest,
13
13
  ImageGenerationResponse,
14
14
  )
15
+ from aidol.schemas.aidol_lead import AIdolLead, AIdolLeadBase, AIdolLeadCreate
15
16
  from aidol.schemas.companion import (
16
17
  Companion,
17
18
  CompanionBase,
18
19
  CompanionCreate,
19
20
  CompanionPublic,
21
+ CompanionStats,
20
22
  CompanionUpdate,
23
+ Gender,
24
+ Grade,
25
+ Position,
21
26
  )
22
27
 
23
28
  __all__ = [
@@ -29,9 +34,16 @@ __all__ = [
29
34
  "ImageGenerationData",
30
35
  "ImageGenerationRequest",
31
36
  "ImageGenerationResponse",
37
+ "AIdolLead",
38
+ "AIdolLeadBase",
39
+ "AIdolLeadCreate",
32
40
  "Companion",
33
41
  "CompanionBase",
34
42
  "CompanionCreate",
35
43
  "CompanionPublic",
44
+ "CompanionStats",
36
45
  "CompanionUpdate",
46
+ "Gender",
47
+ "Grade",
48
+ "Position",
37
49
  ]
@@ -24,20 +24,22 @@ class AIdolBase(BaseModel):
24
24
 
25
25
  model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
26
26
 
27
- name: str = Field(..., description="AIdol group name")
27
+ name: str | None = Field(default=None, description="AIdol group name")
28
+ email: str | None = Field(default=None, description="Creator email")
29
+ greeting: str | None = Field(default=None, description="Greeting message")
28
30
  concept: str | None = Field(default=None, description="Group concept or theme")
29
- profile_image_url: str = Field(..., description="Profile image URL")
31
+ profile_image_url: str | None = Field(default=None, description="Profile image URL")
30
32
 
31
33
 
32
34
  class AIdolCreate(AIdolBase):
33
35
  """Schema for creating an AIdol group (no id).
34
36
 
35
- claim_token is optional for anonymous ownership verification.
37
+ claim_token is required for ownership verification.
36
38
  """
37
39
 
38
- claim_token: str | None = Field(
39
- default=None,
40
- description="Optional client-generated UUID for ownership verification",
40
+ claim_token: str = Field(
41
+ ...,
42
+ description="Client-generated UUID for ownership verification",
41
43
  )
42
44
 
43
45
 
@@ -47,6 +49,8 @@ class AIdolUpdate(BaseModel):
47
49
  model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
48
50
 
49
51
  name: str | None = Field(default=None, description="AIdol group name")
52
+ email: str | None = Field(default=None, description="Creator email")
53
+ greeting: str | None = Field(default=None, description="Greeting message")
50
54
  concept: str | None = Field(default=None, description="Group concept or theme")
51
55
  profile_image_url: str | None = Field(default=None, description="Profile image URL")
52
56
 
@@ -0,0 +1,38 @@
1
+ """
2
+ AIdol Lead (viewer email) schemas
3
+
4
+ Schema hierarchy:
5
+ - AIdolLeadBase: Common fields
6
+ - AIdolLeadCreate: For creating a lead (no id)
7
+ - AIdolLead: Response with all fields
8
+ """
9
+
10
+ from datetime import datetime
11
+
12
+ from humps import camelize
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+
16
+ class AIdolLeadBase(BaseModel):
17
+ """Base AIdol Lead model with common fields."""
18
+
19
+ model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
20
+
21
+ aidol_id: str = Field(..., description="AIdol group ID")
22
+ email: str = Field(..., description="Viewer email")
23
+
24
+
25
+ class AIdolLeadCreate(AIdolLeadBase):
26
+ """Schema for creating an AIdol lead (no id)."""
27
+
28
+
29
+ class AIdolLead(AIdolLeadBase):
30
+ """AIdol Lead response schema with id and timestamps."""
31
+
32
+ model_config = ConfigDict(
33
+ populate_by_name=True, from_attributes=True, alias_generator=camelize
34
+ )
35
+
36
+ id: str = Field(..., description="Lead ID")
37
+ created_at: datetime = Field(..., description="Creation timestamp")
38
+ updated_at: datetime = Field(..., description="Last update timestamp")
@@ -0,0 +1,210 @@
1
+ """
2
+ Companion (member) schemas
3
+
4
+ Schema hierarchy:
5
+ - CompanionStats: Nested stats object for request/response
6
+ - CompanionBase: Mutable fields (used in Create/Update)
7
+ - CompanionCreate: Base + system_prompt (mutable, but sensitive)
8
+ - CompanionUpdate: All fields optional for partial updates
9
+ - Companion: Response with all fields including system_prompt (internal use)
10
+ - CompanionPublic: Response without sensitive fields (API use)
11
+ """
12
+
13
+ from datetime import datetime
14
+ from enum import Enum
15
+
16
+ from humps import camelize
17
+ from pydantic import BaseModel, ConfigDict, Field
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Enums
21
+ # ---------------------------------------------------------------------------
22
+
23
+
24
+ class Gender(str, Enum):
25
+ """Gender options for companions."""
26
+
27
+ MALE = "male"
28
+ FEMALE = "female"
29
+
30
+
31
+ class Grade(str, Enum):
32
+ """Grade levels for companions."""
33
+
34
+ A = "A"
35
+ B = "B"
36
+ C = "C"
37
+ F = "F"
38
+
39
+
40
+ class Position(str, Enum):
41
+ """Position roles in the group."""
42
+
43
+ LEADER = "leader"
44
+ MAIN_VOCAL = "mainVocal"
45
+ SUB_VOCAL = "subVocal"
46
+ MAIN_DANCER = "mainDancer"
47
+ SUB_DANCER = "subDancer"
48
+ MAIN_RAPPER = "mainRapper"
49
+ SUB_RAPPER = "subRapper"
50
+ VISUAL = "visual"
51
+ MAKNAE = "maknae"
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Nested Objects
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ class CompanionStats(BaseModel):
60
+ """Nested stats object for API request/response."""
61
+
62
+ model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
63
+
64
+ vocal: int = Field(default=0, ge=0, le=100, description="Vocal skill")
65
+ dance: int = Field(default=0, ge=0, le=100, description="Dance skill")
66
+ rap: int = Field(default=0, ge=0, le=100, description="Rap skill")
67
+ visual: int = Field(default=0, ge=0, le=100, description="Visual score")
68
+ stamina: int = Field(default=0, ge=0, le=100, description="Stamina")
69
+ charm: int = Field(default=0, ge=0, le=100, description="Charm score")
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Request Schemas
74
+ # ---------------------------------------------------------------------------
75
+
76
+
77
+ class CompanionBase(BaseModel):
78
+ """Base companion model with common mutable fields.
79
+
80
+ Contains fields that can be modified after creation.
81
+ Excludes system_prompt (sensitive, requires explicit inclusion).
82
+ """
83
+
84
+ model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
85
+
86
+ aidol_id: str | None = Field(default=None, description="AIdol group ID")
87
+ name: str | None = Field(default=None, description="Companion name")
88
+ gender: Gender | None = Field(default=None, description="Gender")
89
+ grade: Grade | None = Field(default=None, description="Grade level")
90
+ biography: str | None = Field(default=None, description="Companion biography")
91
+ profile_picture_url: str | None = Field(
92
+ default=None, description="Profile picture URL"
93
+ )
94
+ position: Position | None = Field(default=None, description="Position in group")
95
+
96
+ # MBTI scores (1-10)
97
+ mbti_energy: int | None = Field(default=None, ge=1, le=10, description="E↔I (1-10)")
98
+ mbti_perception: int | None = Field(
99
+ default=None, ge=1, le=10, description="S↔N (1-10)"
100
+ )
101
+ mbti_judgment: int | None = Field(
102
+ default=None, ge=1, le=10, description="T↔F (1-10)"
103
+ )
104
+ mbti_lifestyle: int | None = Field(
105
+ default=None, ge=1, le=10, description="J↔P (1-10)"
106
+ )
107
+
108
+ # Stats (nested object)
109
+ stats: CompanionStats = Field(
110
+ default_factory=CompanionStats, description="Ability stats"
111
+ )
112
+
113
+
114
+ class CompanionCreate(CompanionBase):
115
+ """Schema for creating a companion (no id).
116
+
117
+ Includes system_prompt for creation (excluded from response for security).
118
+ """
119
+
120
+ system_prompt: str | None = Field(
121
+ default=None, description="AI system prompt (not exposed in responses)"
122
+ )
123
+
124
+
125
+ class CompanionUpdate(BaseModel):
126
+ """Schema for updating a companion (all fields optional)."""
127
+
128
+ model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
129
+
130
+ aidol_id: str | None = Field(default=None, description="AIdol group ID")
131
+ name: str | None = Field(default=None, description="Companion name")
132
+ gender: Gender | None = Field(default=None, description="Gender")
133
+ grade: Grade | None = Field(default=None, description="Grade level")
134
+ biography: str | None = Field(default=None, description="Companion biography")
135
+ profile_picture_url: str | None = Field(
136
+ default=None, description="Profile picture URL"
137
+ )
138
+ position: Position | None = Field(default=None, description="Position in group")
139
+ system_prompt: str | None = Field(
140
+ default=None, description="AI system prompt (not exposed in responses)"
141
+ )
142
+
143
+ # MBTI scores (1-10)
144
+ mbti_energy: int | None = Field(default=None, ge=1, le=10, description="E↔I (1-10)")
145
+ mbti_perception: int | None = Field(
146
+ default=None, ge=1, le=10, description="S↔N (1-10)"
147
+ )
148
+ mbti_judgment: int | None = Field(
149
+ default=None, ge=1, le=10, description="T↔F (1-10)"
150
+ )
151
+ mbti_lifestyle: int | None = Field(
152
+ default=None, ge=1, le=10, description="J↔P (1-10)"
153
+ )
154
+
155
+ # Stats (nested object, optional for updates)
156
+ stats: CompanionStats | None = Field(default=None, description="Ability stats")
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Response Schemas
161
+ # ---------------------------------------------------------------------------
162
+
163
+
164
+ class Companion(CompanionBase):
165
+ """Companion response schema with id and timestamps.
166
+
167
+ Includes system_prompt for internal use (Service layer).
168
+ Use CompanionPublic for API responses to exclude sensitive fields.
169
+ """
170
+
171
+ model_config = ConfigDict(
172
+ populate_by_name=True, from_attributes=True, alias_generator=camelize
173
+ )
174
+
175
+ id: str = Field(..., description="Companion ID")
176
+ system_prompt: str | None = Field(
177
+ default=None, description="AI system prompt (sensitive, internal use only)"
178
+ )
179
+ created_at: datetime = Field(..., description="Creation timestamp")
180
+ updated_at: datetime = Field(..., description="Last update timestamp")
181
+
182
+
183
+ class CompanionPublic(BaseModel):
184
+ """Public companion response schema for frontend.
185
+
186
+ - Excludes system_prompt for security
187
+ - Uses nested stats object
188
+ - Includes calculated mbti string
189
+ """
190
+
191
+ model_config = ConfigDict(
192
+ populate_by_name=True, from_attributes=True, alias_generator=camelize
193
+ )
194
+
195
+ id: str = Field(..., description="Companion ID")
196
+ aidol_id: str | None = Field(default=None, description="AIdol group ID")
197
+ name: str | None = Field(default=None, description="Companion name")
198
+ gender: Gender | None = Field(default=None, description="Gender")
199
+ grade: Grade | None = Field(default=None, description="Grade level")
200
+ biography: str | None = Field(default=None, description="Companion biography")
201
+ profile_picture_url: str | None = Field(
202
+ default=None, description="Profile picture URL"
203
+ )
204
+ position: Position | None = Field(default=None, description="Position in group")
205
+ mbti: str | None = Field(default=None, description="Calculated MBTI (e.g., ENFP)")
206
+ stats: CompanionStats = Field(
207
+ default_factory=CompanionStats, description="Ability stats"
208
+ )
209
+ created_at: datetime = Field(..., description="Creation timestamp")
210
+ updated_at: datetime = Field(..., description="Last update timestamp")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "py-aidol"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Create and chat with your own AI idol group"
5
5
  authors = ["AIoIA, Inc. <devops@aioia.ai>"]
6
6
  readme = "README.md"
@@ -1,27 +0,0 @@
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"),)
@@ -1,93 +0,0 @@
1
- """
2
- Companion (member) schemas
3
-
4
- Schema hierarchy:
5
- - CompanionBase: Mutable fields (used in Create/Update)
6
- - CompanionCreate: Base + system_prompt (mutable, but sensitive)
7
- - CompanionUpdate: Base + system_prompt (mutable, but sensitive)
8
- - Companion: Response with all fields including system_prompt (internal use)
9
- - CompanionPublic: Response without sensitive fields (API use)
10
- """
11
-
12
- from datetime import datetime
13
-
14
- from humps import camelize
15
- from pydantic import BaseModel, ConfigDict, Field
16
-
17
-
18
- class CompanionBase(BaseModel):
19
- """Base companion model with common mutable fields.
20
-
21
- Contains fields that can be modified after creation.
22
- Excludes system_prompt (sensitive, requires explicit inclusion).
23
- """
24
-
25
- model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
26
-
27
- aidol_id: str | None = Field(default=None, description="AIdol group ID")
28
- name: str = Field(..., description="Companion name")
29
- biography: str | None = Field(default=None, description="Companion biography")
30
- profile_picture_url: str | None = Field(
31
- default=None, description="Profile picture URL"
32
- )
33
-
34
-
35
- class CompanionCreate(CompanionBase):
36
- """Schema for creating a companion (no id).
37
-
38
- Includes system_prompt for creation (excluded from response for security).
39
- """
40
-
41
- system_prompt: str | None = Field(
42
- default=None, description="AI system prompt (not exposed in responses)"
43
- )
44
-
45
-
46
- class CompanionUpdate(BaseModel):
47
- """Schema for updating a companion (all fields optional)."""
48
-
49
- model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
50
-
51
- aidol_id: str | None = Field(default=None, description="AIdol group ID")
52
- name: str | None = Field(default=None, description="Companion name")
53
- biography: str | None = Field(default=None, description="Companion biography")
54
- profile_picture_url: str | None = Field(
55
- default=None, description="Profile picture URL"
56
- )
57
- system_prompt: str | None = Field(
58
- default=None, description="AI system prompt (not exposed in responses)"
59
- )
60
-
61
-
62
- class Companion(CompanionBase):
63
- """Companion response schema with id and timestamps.
64
-
65
- Includes system_prompt for internal use (Service layer).
66
- Use CompanionPublic for API responses to exclude sensitive fields.
67
- """
68
-
69
- model_config = ConfigDict(
70
- populate_by_name=True, from_attributes=True, alias_generator=camelize
71
- )
72
-
73
- id: str = Field(..., description="Companion ID")
74
- system_prompt: str | None = Field(
75
- default=None, description="AI system prompt (sensitive, internal use only)"
76
- )
77
- created_at: datetime = Field(..., description="Creation timestamp")
78
- updated_at: datetime = Field(..., description="Last update timestamp")
79
-
80
-
81
- class CompanionPublic(CompanionBase):
82
- """Public companion response schema without sensitive fields.
83
-
84
- Excludes system_prompt for API responses.
85
- """
86
-
87
- model_config = ConfigDict(
88
- populate_by_name=True, from_attributes=True, alias_generator=camelize
89
- )
90
-
91
- id: str = Field(..., description="Companion ID")
92
- created_at: datetime = Field(..., description="Creation timestamp")
93
- updated_at: datetime = Field(..., description="Last update timestamp")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes