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

allow user to append own source content #15

Merged
merged 45 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6bd9abc
add accordion item for creating custom source items
nwaughachukwuma Nov 20, 2024
ac8a256
add shadcn-card component
nwaughachukwuma Nov 20, 2024
dfbdc90
wip: add form for custom user source
nwaughachukwuma Nov 20, 2024
2b2f5f1
cleanup custom source componentn
nwaughachukwuma Nov 20, 2024
f1b8ab2
add shadcn input component
nwaughachukwuma Nov 20, 2024
1bf978c
add components to get custom source by website url and copy/paste
nwaughachukwuma Nov 20, 2024
6c011fd
add logic for extracting pdf and page content from url
nwaughachukwuma Nov 20, 2024
32586d8
wip: add component for rendering pdf content
nwaughachukwuma Nov 20, 2024
cbff60a
define id field on URLContent
nwaughachukwuma Nov 20, 2024
d961077
fetch custom sources from specified links
nwaughachukwuma Nov 20, 2024
4dce53e
retain formatting in web and pdf content; improve page content rendering
nwaughachukwuma Nov 21, 2024
2b93957
cleanup
nwaughachukwuma Nov 21, 2024
60652ba
wip: add logic to store and fetch custom sources
nwaughachukwuma Nov 21, 2024
71b812a
add endpoint to fetch custom sources for a given audiocast session
nwaughachukwuma Nov 21, 2024
0149a11
create a component for rendering audio_souces - ai-generated custom
nwaughachukwuma Nov 21, 2024
d839f66
setup firebase and rxjs
nwaughachukwuma Nov 21, 2024
39e45ab
add a safe_to_dict method to db_manager
nwaughachukwuma Nov 21, 2024
3cd2a89
use generate-custom-source endpoint when adding custom source
nwaughachukwuma Nov 21, 2024
a9dcbbd
fetch custom sources on mount page/drawer
nwaughachukwuma Nov 21, 2024
0ac5bb9
read custom_sources from firebase using the js_sdk
nwaughachukwuma Nov 21, 2024
a358954
add firebase.json and optimize_deps
nwaughachukwuma Nov 21, 2024
4f8e2d7
parse firebase_config env variable in vite.config
nwaughachukwuma Nov 22, 2024
b2389a6
define .firebaserc
nwaughachukwuma Nov 22, 2024
873934b
update custom_source model; add toast notification
nwaughachukwuma Nov 22, 2024
68dbc6e
consolidate and cleanup
nwaughachukwuma Nov 22, 2024
9ba2940
wip: create workflow to store copy/paste source
nwaughachukwuma Nov 22, 2024
f8f4c44
add useful helpers to CustomSourceManager
nwaughachukwuma Nov 22, 2024
e90a2fb
add endpoint and request logic to add copy/paste source
nwaughachukwuma Nov 22, 2024
dbcc992
improve the layouts of copypast_source and website_url_source forms
nwaughachukwuma Nov 22, 2024
226fbad
refactor and cleanup
nwaughachukwuma Nov 22, 2024
0169e7d
use improved module names
nwaughachukwuma Nov 22, 2024
79b758b
finalize endpoint to store uploaded content
nwaughachukwuma Nov 22, 2024
6e2da44
improve class and module names
nwaughachukwuma Nov 22, 2024
d95073e
add ui logic for uploading dropped files
nwaughachukwuma Nov 22, 2024
4f9e69d
upload dragged&dropped file as form_data; allow up to 10mb
nwaughachukwuma Nov 22, 2024
f1d56cd
add a title field on source_content model
nwaughachukwuma Nov 22, 2024
9b6e4bc
ensure the user cannot create more than 5 custom sources
nwaughachukwuma Nov 22, 2024
dac47b2
allow uploading a file using the file dialog
nwaughachukwuma Nov 22, 2024
4f9520c
cleanup
nwaughachukwuma Nov 22, 2024
e9e6c9c
write source_content to db after it's generated
nwaughachukwuma Nov 22, 2024
df49a87
add user provided custom sources to the tts_prompt
nwaughachukwuma Nov 22, 2024
8566d73
specify the correct interface for _get_custom_sources
nwaughachukwuma Nov 22, 2024
a4477eb
improve the prompt to exclude extraneous information in user-provided…
nwaughachukwuma Nov 22, 2024
1986c0a
add FIREBASE_CONFIG to env
nwaughachukwuma Nov 22, 2024
60d3464
remove firebase_config from env_vars
nwaughachukwuma Nov 22, 2024
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
3 changes: 3 additions & 0 deletions .firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"projects": {}
}
1 change: 1 addition & 0 deletions .github/workflows/deploy_app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ env:
CONFIG_FILE: "app/app.yaml"
SERVICE: audiora-app
API_BASE_URL: ${{ secrets.API_BASE_URL }}
FIREBASE_CONFIG: ${{ secrets.FIREBASE_CONFIG }}

jobs:
prepare:
Expand Down
6 changes: 5 additions & 1 deletion api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ uvicorn
uvloop
redis[hiredis]

async-web-search
async-web-search

lxml
beautifulsoup4
pypdf[crypto]
82 changes: 72 additions & 10 deletions api/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from time import time
from typing import Any, Callable, Generator

from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
from fastapi import BackgroundTasks, FastAPI, Form, HTTPException, Request, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi_utilities import add_timer_middleware
Expand All @@ -13,6 +13,18 @@
SessionChatItem,
SessionChatRequest,
)
from src.utils.custom_sources.base_utils import SourceContent
from src.utils.custom_sources.extract_url_content import ExtractURLContent, ExtractURLContentRequest
from src.utils.custom_sources.generate_url_source import (
CustomSourceManager,
CustomSourceModel,
DeleteCustomSourcesRequest,
GenerateCustomSourceRequest,
GetCustomSourcesRequest,
generate_custom_source,
)
from src.utils.custom_sources.save_copied_source import CopiedPasteSourceRequest, save_copied_source
from src.utils.custom_sources.save_uploaded_sources import UploadedFiles
from src.utils.generate_audiocast import (
GenerateAudioCastRequest,
GenerateAudioCastResponse,
Expand Down Expand Up @@ -54,12 +66,16 @@ async def log_request_headers(request: Request, call_next: Callable):


@app.get("/")
async def root():
def root():
return {"message": "Hello World"}


@app.post("/chat/{session_id}", response_model=Generator[str, Any, None])
async def chat_endpoint(session_id: str, request: SessionChatRequest, background_tasks: BackgroundTasks):
def chat_endpoint(
session_id: str,
request: SessionChatRequest,
background_tasks: BackgroundTasks,
):
"""Chat endpoint"""
category = request.contentCategory
db = SessionManager(session_id, category)
Expand All @@ -85,18 +101,20 @@ async def generate_audiocast_endpoint(
request: GenerateAudioCastRequest,
background_tasks: BackgroundTasks,
):
result = await generate_audiocast(request, background_tasks)
return result
return await generate_audiocast(request, background_tasks)


@app.get("/audiocast/{session_id}", response_model=GenerateAudioCastResponse)
async def get_audiocast_endpoint(session_id: str):
def get_audiocast_endpoint(session_id: str):
result = get_audiocast(session_id)
return result


@app.post("/generate-audiocast-source", response_model=str)
async def generate_audiocast_source_endpoint(request: GenerateAudiocastSource, background_tasks: BackgroundTasks):
async def generate_audiocast_source_endpoint(
request: GenerateAudiocastSource,
background_tasks: BackgroundTasks,
):
source_content = await generate_audiocast_source(request, background_tasks)
if not source_content:
raise HTTPException(status_code=500, detail="Failed to generate source content")
Expand Down Expand Up @@ -132,6 +150,50 @@ async def get_signed_url_endpoint(blobname: str):


@app.post("/get-session-title", response_model=str)
async def get_session_title_endpoint(request: GetSessionTitleModel, background_tasks: BackgroundTasks):
source_content = await get_session_title(request, background_tasks)
return source_content
async def get_session_title_endpoint(
request: GetSessionTitleModel,
background_tasks: BackgroundTasks,
):
return await get_session_title(request, background_tasks)


@app.post("/extract-url-content", response_model=SourceContent)
def extract_url_content_endpoint(request: ExtractURLContentRequest):
extractor = ExtractURLContent()
page_content = extractor._extract(request.url)
return page_content.model_dump()


@app.post("/generate-url-source", response_model=SourceContent)
def generate_url_source_endpoint(
request: GenerateCustomSourceRequest,
background_tasks: BackgroundTasks,
):
return generate_custom_source(request, background_tasks)


@app.post("/get-custom-sources", response_model=list[CustomSourceModel])
async def get_custom_sources_endpoint(request: GetCustomSourcesRequest):
return CustomSourceManager(request.sessionId)._get_custom_sources()


@app.post("/delete-custom-source", response_model=list[CustomSourceModel])
def delete_custom_source_endpoint(request: DeleteCustomSourcesRequest):
manager = CustomSourceManager(request.sessionId)
manager._delete_custom_source(request.sourceId)
return "Deleted"


@app.post("/save-copied-source", response_model=str)
def save_copied_source_endpoint(request: CopiedPasteSourceRequest):
result = save_copied_source(request)
return result


@app.post("/save-uploaded-sources", response_model=str)
async def save_uploaded_files_endpoint(files: list[UploadFile], sessionId: str = Form(...)):
"""
Save sources uploaded from the frontend
"""
result = await UploadedFiles(session_id=sessionId)._save_sources(files)
return result
14 changes: 14 additions & 0 deletions api/src/services/firestore_sdk.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime
from typing import Dict, Literal

from firebase_admin.firestore import client, firestore
Expand Down Expand Up @@ -48,3 +49,16 @@ def _get_document(self, collection: Collection, doc_id: str):

def _get_documents(self, collection: Collection):
return self._get_collection(collection).stream()

@classmethod
def _safe_to_dict(cls, data: dict):
"""
safely parse firestore data by converting convert all timestamp to string
"""

def _safe_to_str(value: dict | str | datetime):
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %H:%M:%S")
return value

return {k: _safe_to_str(v) for k, v in data.items()}
10 changes: 4 additions & 6 deletions api/src/utils/audiocast_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,20 @@ async def _run(self):
Returns:
str: The audiocast source content
"""
source_content = await self.__use_openai(self.category, self.preference_summary)
additional_ctx = await self.get_context(self.preference_summary)
source_content = await self.__use_openai(self.category, self.preference_summary, additional_ctx)
if not source_content:
raise ValueError("Failed to generate audiocast source content")

return self._refine(source_content)

async def __use_openai(self, category: ContentCategory, preference_summary: str):
async def __use_openai(self, category: ContentCategory, preference_summary: str, additional_ctx: str):
"""
Generate audiocast source content using OpenAI.
"""
refined_summary = re.sub("You want", "A user who wants", preference_summary, flags=re.IGNORECASE)
refined_summary = re.sub("You", "A user", refined_summary, flags=re.IGNORECASE)

additional_context = await self.get_context(self.preference_summary)
print(f">>> Additional context: {additional_context}")

response = get_openai().chat.completions.create(
model="gpt-4o",
messages=[
Expand All @@ -45,7 +43,7 @@ async def __use_openai(self, category: ContentCategory, preference_summary: str)
"content": generate_source_content_prompt(
category,
refined_summary,
additional_context,
additional_ctx,
),
},
{
Expand Down
7 changes: 5 additions & 2 deletions api/src/utils/audiocast_script_maker.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
class AudioScriptMaker:
category: ContentCategory

def __init__(self, category: ContentCategory, source_content: str):
def __init__(self, category: ContentCategory, source_content: str, compiled_custom_sources: str | None = None):
self.category = category
self.source_content = source_content
self.compiled_custom_sources = compiled_custom_sources

def create(self, provider: AudioScriptProvider = "openai"):
"""
Expand All @@ -28,9 +29,11 @@ def create(self, provider: AudioScriptProvider = "openai"):
"""
print("Generating audio script...")
print(f"Category: {self.category}; Source content: {self.source_content}")
if self.compiled_custom_sources:
print(f"Custom sources: {self.compiled_custom_sources}")

prompt_maker = TTSPromptMaker(self.category, Metadata())
system_prompt = prompt_maker.get_system_prompt(self.source_content)
system_prompt = prompt_maker.get_system_prompt(self.source_content, self.compiled_custom_sources)

if provider == "anthropic":
audio_script = self.__use_anthropic(system_prompt)
Expand Down
2 changes: 1 addition & 1 deletion api/src/utils/audiocast_source_refiner.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def __init__(self, category: ContentCategory, preference_summary: str):
self.category = category
self.preference_summary = preference_summary

def _refine(self, content):
def _refine(self, content: str):
"""
Moderate and augment the source content to ensure it aligns with the user's preferences.
"""
Expand Down
Empty file.
105 changes: 105 additions & 0 deletions api/src/utils/custom_sources/base_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from typing import Literal, Optional, TypedDict, cast

from google.cloud.firestore_v1 import DocumentReference
from pydantic import BaseModel

from src.services.firestore_sdk import (
Collection,
DBManager,
collections,
)


class SourceContent(BaseModel):
id: str
content: str
content_type: str
metadata: dict = {}
title: Optional[str] = None

def __str__(self):
result = f"Content: {self.content}"
if self.title:
return f"Title: {self.title}\n{result}"
return result


class CustomSourceModel(SourceContent):
source_type: Literal["link", "copy/paste", "file_upload"]
url: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None


class SourceContentDict(TypedDict):
id: str
content: str
content_type: str
metadata: dict
title: Optional[str]


class CustomSourceModelDict(SourceContentDict):
source_type: Literal["link", "copy/paste", "file_upload"]
url: Optional[str]
created_at: Optional[str]
updated_at: Optional[str]


class CustomSourceManager(DBManager):
collection: Collection = collections["audiora_sessions"]
sub_collection = "custom_sources"

def __init__(self, session_id: str):
super().__init__()
self.doc_id = session_id

def _check_document(self):
"""if the collection does not exist, create it"""
doc = self._get_document(self.collection, self.doc_id)
if not doc.exists:
raise Exception("Session not found")
return doc

def _get_doc_ref(self, source_id: str) -> DocumentReference:
self._check_document()
return (
self._get_collection(self.collection)
.document(self.doc_id)
.collection(self.sub_collection)
.document(source_id)
)

def _set_custom_source(self, data: CustomSourceModel):
return self._get_doc_ref(data.id).set(
{
**(data.model_dump()),
"created_at": self._timestamp,
"updated_at": self._timestamp,
}
)

def _get_custom_source(self, source_id: str) -> CustomSourceModel | None:
doc = self._get_doc_ref(source_id).get()
data = doc.to_dict()
if doc.exists and data:
return cast(CustomSourceModel, self._safe_to_dict(data))

def _get_custom_sources(self) -> list[CustomSourceModelDict]:
self._check_document()

try:
session_ref = self._get_collection(self.collection).document(self.doc_id)
docs = session_ref.collection(self.sub_collection).get()
return [
cast(CustomSourceModelDict, self._safe_to_dict(doc.to_dict()))
for doc in docs
if doc.exists and doc.to_dict()
]

except Exception as e:
print(f"Error getting custom sources for Session: {self.doc_id}", e)
return []

def _delete_custom_source(self, source_id: str):
return self._get_doc_ref(source_id).delete()
Loading
Loading