Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] - Add CRUD Operations for Personas #4

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions apis/paios/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,91 @@ paths:
description: User Not Found
'409':
description: Email Already Taken
/personas:
get:
tags:
- Persona Management
summary: Retrieve all personas
description: Get all personas.
parameters:
- $ref: '#/components/parameters/sort'
- $ref: '#/components/parameters/range'
- $ref: '#/components/parameters/filter'
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Persona'
headers:
X-Total-Count:
$ref: '#/components/headers/X-Total-Count'
post:
summary: Create new persona
tags:
- Persona Management
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/Persona'
'400':
description: Missing Required Information
description: Creates a new persona.
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PersonaCreate'
'/personas/{id}':
get:
tags:
- Persona Management
summary: Retrieve persona by id
description: Retrieve the information of the persona with the matching persona ID.
parameters:
- $ref: '#/components/parameters/id'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Persona'
put:
summary: Update persona by id
tags:
- Persona Management
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Persona'
description: Updates the persona with the given id.
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Persona'
delete:
summary: Delete persona by id
description: Deletes the persona with the given id.
tags:
- Persona Management
responses:
'204':
description: No Content
'404':
description: Not Found
parameters:
- $ref: '#/components/parameters/id'
tags:
- name: Abilities Management
description: Installation and configuration of abilities
Expand All @@ -562,6 +647,8 @@ tags:
description: Management of downloads
- name: User Management
description: Management of user accounts
- name: Persona Management
description: Management of personas
components:
headers:
X-Total-Count:
Expand Down Expand Up @@ -959,3 +1046,36 @@ components:
required:
- name
- email
Persona:
type: object
title: Persona
properties:
id:
$ref: '#/components/schemas/uuid4ReadOnly'
name:
$ref: '#/components/schemas/textShort'
description:
$ref: '#/components/schemas/textLong'
voiceId:
$ref: '#/components/schemas/uuid4ReadOnly'
faceId:
$ref: '#/components/schemas/uuid4ReadOnly'
required:
- name
PersonaCreate:
type: object
title: PersonaCreate
description: Persona without id which is server-generated.
properties:
id:
$ref: '#/components/schemas/uuid4ReadOnly'
name:
$ref: '#/components/schemas/textShort'
description:
$ref: '#/components/schemas/textLong'
voiceId:
$ref: '#/components/schemas/uuid4'
faceId:
$ref: '#/components/schemas/uuid4'
required:
- name
37 changes: 37 additions & 0 deletions apis/paios/personas.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# API testing with Visual Studio Code - REST Client Extention

POST http://localhost:3080/api/v1/personas
Authorization: Bearer {{PAIOS_BEARER_TOKEN}}
Content-Type: application/json

{
"name": "Attention Is All You Need",
"description": "Ashish Vaswani et al",
"voiceId": "Artificial Intelligence",
"faceId": "We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely."
}

###

PUT http://localhost:3080/api/v1/personas/1cbb0bc5-bae2-4b9d-9555-f2282f767047
Authorization: Bearer {{PAIOS_BEARER_TOKEN}}
Content-Type: application/json

{
"name": "Generative Adversarial Networks",
"description": "Goodfellow et al",
"voiceId": "Artificial Intelligence",
"faceId": "We propose a new framework for estimating generative models via an adversarial process, in which we simultaneously train two models: a generative model G that captures the data distribution, and a discriminative model D that estimates the probability that a sample came from the training data rather than G."
}

###

GET http://localhost:3080/api/v1/personas/1cbb0bc5-bae2-4b9d-9555-f2282f767047
Authorization: Bearer {{PAIOS_BEARER_TOKEN}}
Content-Type: application/json

###

DELETE http://localhost:3080/api/v1/personas/1cbb0bc5-bae2-4b9d-9555-f2282f767047
Authorization: Bearer {{PAIOS_BEARER_TOKEN}}
Content-Type: application/json
66 changes: 66 additions & 0 deletions backend/api/PersonasView.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from starlette.responses import JSONResponse, Response
from backend.managers.PersonasManager import PersonasManager
from common.paths import api_base_url
from backend.pagination import parse_pagination_params
import logging

logger = logging.getLogger(__name__)
class PersonasView:
def __init__(self):
self.pm = PersonasManager()

async def get(self, id: str):
persona = await self.pm.retrieve_persona(id)
if persona is None:
return JSONResponse({"error": "Persona not found"}, status_code=404)
return JSONResponse(persona, status_code=200)

async def post(self, body: dict):
persona_data = {
'name': body.get('name'),
'description': body.get('description'),
'voiceId': body.get('voiceId'),
'faceId': body.get('faceId')
}
id = await self.pm.create_persona(**persona_data)
persona = await self.pm.retrieve_persona(id)
return JSONResponse(persona, status_code=201, headers={'Location': f'{api_base_url}/personas/{id}'})

async def put(self, id: str, body: dict):
persona_data = {
'name': body.get('name'),
'description': body.get('description'),
'voiceId': body.get('voiceId'),
'faceId': body.get('faceId')
}
await self.pm.update_persona(id, **persona_data)
persona = await self.pm.retrieve_persona(id)
return JSONResponse(persona, status_code=200)

async def delete(self, id: str):
await self.pm.delete_persona(id)
return Response(status_code=204)

async def search(self, filter: str = None, range: str = None, sort: str = None):
result = parse_pagination_params(filter, range, sort)
if isinstance(result, JSONResponse):
return result

offset, limit, sort_by, sort_order, filters = result

# Ensure filters is a dictionary
if filters is None:
filters = {}

personas, total_count = await self.pm.retrieve_personas(
limit=limit,
offset=offset,
sort_by=sort_by,
sort_order=sort_order,
filters=filters
)
headers = {
'X-Total-Count': str(total_count),
'Content-Range': f'personas {offset}-{offset + len(personas) - 1}/{total_count}'
}
return JSONResponse(personas, status_code=200, headers=headers)
1 change: 1 addition & 0 deletions backend/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from .ConfigView import ConfigView
from .DownloadsView import DownloadsView
from .UsersView import UsersView
from .PersonasView import PersonasView
104 changes: 104 additions & 0 deletions backend/managers/PersonasManager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from uuid import uuid4
import backend.db as db
from backend.utils import remove_null_fields, zip_fields
from threading import Lock
import logging

logger = logging.getLogger(__name__)

class PersonasManager:
_instance = None
_lock = Lock()

def __new__(cls, *args, **kwargs):
if not cls._instance:
with cls._lock:
if not cls._instance:
cls._instance = super(PersonasManager, cls).__new__(cls, *args, **kwargs)
return cls._instance

def __init__(self):
if not hasattr(self, '_initialized'):
with self._lock:
if not hasattr(self, '_initialized'):
db.init_db()
self._initialized = True

async def create_persona(self, name, description, voiceId, faceId):
id = str(uuid4())
query = 'INSERT INTO persona (id, name, description, voiceId, faceId) VALUES (?, ?, ?, ?, ?)'
await db.execute_query(query, (id, name, description, voiceId, faceId))
return id

async def update_persona(self, id, name, description, voiceId, faceId):
query = 'INSERT OR REPLACE INTO persona (id, name, description, voiceId, faceId) VALUES (?, ?, ?, ?, ?)'
return await db.execute_query(query, (id, name, description, voiceId, faceId))

async def delete_persona(self, id):
query = 'DELETE FROM persona WHERE id = ?'
return await db.execute_query(query, (id,))

async def retrieve_persona(self, id):
query = 'SELECT id, name, description, voiceId, faceId FROM persona WHERE id = ?'
result = await db.execute_query(query, (id,))
if result:
fields = ['id', 'name', 'description', 'voiceId', 'faceId']
persona = remove_null_fields(zip_fields(fields, result[0]))
persona['id'] = id
return persona
return None

async def retrieve_personas(self, offset=0, limit=100, sort_by=None, sort_order='asc', filters=None):
# Initialize filters to an empty dictionary if it is None
if filters is None:
filters = {}

base_query = 'SELECT id, name, description, voiceId, faceId FROM persona'
query_params = []

# Apply filters
filter_clauses = []
if filters:
for key, value in filters.items():
if key == 'name':
filter_clauses.append(f"LOWER(name) LIKE ?")
query_params.append(f"%{value.lower()}%")
else:
if isinstance(value, list):
placeholders = ', '.join(['?'] * len(value))
filter_clauses.append(f"{key} IN ({placeholders})")
query_params.extend(value)
else:
filter_clauses.append(f"{key} = ?")
query_params.append(value)

if filter_clauses:
base_query += ' WHERE ' + ' AND '.join(filter_clauses)

# Validate and apply sorting
valid_sort_columns = ['id', 'name', 'description', 'voiceId', 'faceId']
if sort_by and sort_by in valid_sort_columns:
sort_order = 'DESC' if sort_order.lower() == 'desc' else 'ASC'
base_query += f' ORDER BY {sort_by} {sort_order}'

# Apply pagination
base_query += ' LIMIT ? OFFSET ?'
query_params.extend([limit, offset])

# Execute the main query
logger.info("base_query = %s", base_query)
logger.info("query_params = %s", query_params)
results = await db.execute_query(base_query, tuple(query_params))

fields = ['id', 'name', 'description', 'voiceId', 'faceId']
personas = [remove_null_fields(zip_fields(fields, result)) for result in results]

# Get the total count of personas
total_count_query = 'SELECT COUNT(*) FROM persona'
total_count_params = query_params[:-2] # Exclude limit and offset for the count query
if filter_clauses:
total_count_query += ' WHERE ' + ' AND '.join(filter_clauses)
total_count_result = await db.execute_query(total_count_query, tuple(total_count_params))
total_count = total_count_result[0][0] if total_count_result else 0

return personas, total_count
4 changes: 3 additions & 1 deletion backend/managers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .ConfigManager import ConfigManager
from .DownloadsManager import DownloadsManager
from .UsersManager import UsersManager
from .PersonasManager import PersonasManager

# List of manager classes
manager_classes = [
Expand All @@ -12,7 +13,8 @@
ChannelsManager,
ConfigManager,
DownloadsManager,
UsersManager
UsersManager,
PersonasManager
]

# Initialize the managers dynamically
Expand Down
7 changes: 7 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ class Asset(SQLModel, table=True):
creator: str = Field(nullable=True)
subject: str = Field(nullable=True)
description: str = Field(nullable=True)

class Persona(SQLModel, table=True):
id: str = Field(default=None, primary_key=True)
name: str = Field(nullable=False)
description: str = Field(nullable=True)
voiceId: str = Field(nullable=True)
faceId: str = Field(nullable=True)
Loading
Loading