diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 006c674..a6ad8f7 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: postgres: image: postgres:16-alpine @@ -45,14 +43,33 @@ services: postgres: condition: service_healthy volumes: - # Mount source code for hot reload - - ./services/raggr:/app - # Exclude node_modules and Python cache - - /app/raggr-frontend/node_modules - - /app/__pycache__ - # Persist data + # Persist data only - chromadb_data:/app/chromadb - command: sh -c "chmod +x /app/startup-dev.sh && /app/startup-dev.sh" + # Share frontend dist with frontend container + - frontend_dist:/app/raggr-frontend/dist + develop: + watch: + # Sync Python source files + - action: sync + path: ./services/raggr + target: /app + ignore: + - raggr-frontend/ + - __pycache__/ + - "*.pyc" + - "*.pyo" + - "*.pyd" + - .git/ + - chromadb/ + # Sync+restart on frontend dist changes + - action: sync+restart + path: ./services/raggr/raggr-frontend/dist + target: /app/raggr-frontend/dist + # Restart on dependency changes + - action: rebuild + path: ./services/raggr/pyproject.toml + - action: rebuild + path: ./services/raggr/uv.lock raggr-frontend: build: @@ -61,12 +78,25 @@ services: environment: - NODE_ENV=development volumes: - # Mount source code for hot reload - - ./services/raggr/raggr-frontend:/app - # Exclude node_modules to use container's version - - /app/node_modules - command: sh -c "yarn build && yarn watch:build" + # Share dist folder with backend + - frontend_dist:/app/dist + develop: + watch: + # Sync frontend source files + - action: sync + path: ./services/raggr/raggr-frontend + target: /app + ignore: + - node_modules/ + - dist/ + - .git/ + # Rebuild on dependency changes + - action: rebuild + path: ./services/raggr/raggr-frontend/package.json + - action: rebuild + path: ./services/raggr/raggr-frontend/yarn.lock volumes: chromadb_data: postgres_data: + frontend_dist: diff --git a/services/raggr/Dockerfile.dev b/services/raggr/Dockerfile.dev index d5719a4..b5984af 100644 --- a/services/raggr/Dockerfile.dev +++ b/services/raggr/Dockerfile.dev @@ -24,10 +24,16 @@ RUN mkdir -p /app/chromadb /app/database # Expose port EXPOSE 8080 +# Copy application source code +COPY . . + +# Make startup script executable +RUN chmod +x /app/startup-dev.sh + # Set environment variables ENV PYTHONPATH=/app ENV CHROMADB_PATH=/app/chromadb ENV PYTHONUNBUFFERED=1 -# The actual source code will be mounted as a volume -# No CMD here - will be specified in docker-compose +# Default command +CMD ["/app/startup-dev.sh"] diff --git a/services/raggr/app.py b/services/raggr/app.py index 2bfa02c..cc18e0e 100644 --- a/services/raggr/app.py +++ b/services/raggr/app.py @@ -26,8 +26,7 @@ app.register_blueprint(blueprints.conversation.conversation_blueprint) # Database configuration with environment variable support DATABASE_URL = os.getenv( - "DATABASE_URL", - "postgres://raggr:raggr_dev_password@localhost:5432/raggr" + "DATABASE_URL", "postgres://raggr:raggr_dev_password@localhost:5432/raggr" ) TORTOISE_CONFIG = { @@ -123,10 +122,17 @@ async def get_messages(): } ) + name = conversation.name + if len(messages) > 8: + name = await blueprints.conversation.logic.rename_conversation( + user=user, + conversation=conversation, + ) + return jsonify( { "id": str(conversation.id), - "name": conversation.name, + "name": name, "messages": messages, "created_at": conversation.created_at.isoformat(), "updated_at": conversation.updated_at.isoformat(), diff --git a/services/raggr/blueprints/conversation/__init__.py b/services/raggr/blueprints/conversation/__init__.py index edf5e7c..dbb38ed 100644 --- a/services/raggr/blueprints/conversation/__init__.py +++ b/services/raggr/blueprints/conversation/__init__.py @@ -1,27 +1,31 @@ import datetime +from quart import Blueprint, jsonify from quart_jwt_extended import ( - jwt_refresh_token_required, get_jwt_identity, + jwt_refresh_token_required, ) -from quart import Blueprint, jsonify +import blueprints.users.models + +from .logic import rename_conversation from .models import ( Conversation, PydConversation, PydListConversation, ) -import blueprints.users.models - conversation_blueprint = Blueprint( "conversation_api", __name__, url_prefix="/api/conversation" ) @conversation_blueprint.route("/") +@jwt_refresh_token_required async def get_conversation(conversation_id: str): conversation = await Conversation.get(id=conversation_id) + current_user_uuid = get_jwt_identity() + user = await blueprints.users.models.User.get(id=current_user_uuid) await conversation.fetch_related("messages") # Manually serialize the conversation with messages @@ -35,11 +39,18 @@ async def get_conversation(conversation_id: str): "created_at": msg.created_at.isoformat(), } ) + name = conversation.name + if len(messages) > 8 and "datetime" in name.lower(): + name = await rename_conversation( + user=user, + conversation=conversation, + ) + print(name) return jsonify( { "id": str(conversation.id), - "name": conversation.name, + "name": name, "messages": messages, "created_at": conversation.created_at.isoformat(), "updated_at": conversation.updated_at.isoformat(), diff --git a/services/raggr/blueprints/conversation/logic.py b/services/raggr/blueprints/conversation/logic.py index 1b4a18e..5359386 100644 --- a/services/raggr/blueprints/conversation/logic.py +++ b/services/raggr/blueprints/conversation/logic.py @@ -1,9 +1,10 @@ import tortoise.exceptions - -from .models import Conversation, ConversationMessage +from langchain_openai import ChatOpenAI import blueprints.users.models +from .models import Conversation, ConversationMessage, RenameConversationOutputSchema + async def create_conversation(name: str = "") -> Conversation: conversation = await Conversation.create(name=name) @@ -58,3 +59,22 @@ async def get_conversation_transcript( messages.append(f"{message.speaker} at {message.created_at}: {message.text}") return "\n".join(messages) + + +async def rename_conversation( + user: blueprints.users.models.User, + conversation: Conversation, +) -> str: + messages: str = await get_conversation_transcript( + user=user, conversation=conversation + ) + + llm = ChatOpenAI(model="gpt-4o-mini") + structured_llm = llm.with_structured_output(RenameConversationOutputSchema) + + prompt = f"Summarize the following conversation into a sassy one-liner title:\n\n{messages}" + response = structured_llm.invoke(prompt) + new_name: str = response.get("title") + conversation.name = new_name + await conversation.save() + return new_name diff --git a/services/raggr/blueprints/conversation/models.py b/services/raggr/blueprints/conversation/models.py index ac76b24..e0e5ad1 100644 --- a/services/raggr/blueprints/conversation/models.py +++ b/services/raggr/blueprints/conversation/models.py @@ -1,11 +1,18 @@ import enum +from dataclasses import dataclass -from tortoise.models import Model from tortoise import fields from tortoise.contrib.pydantic import ( - pydantic_queryset_creator, pydantic_model_creator, + pydantic_queryset_creator, ) +from tortoise.models import Model + + +@dataclass +class RenameConversationOutputSchema: + title: str + justification: str class Speaker(enum.Enum): diff --git a/services/raggr/pyproject.toml b/services/raggr/pyproject.toml index 2eab225..dd96d70 100644 --- a/services/raggr/pyproject.toml +++ b/services/raggr/pyproject.toml @@ -4,7 +4,34 @@ version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" -dependencies = ["chromadb>=1.1.0", "python-dotenv>=1.0.0", "flask>=3.1.2", "httpx>=0.28.1", "ollama>=0.6.0", "openai>=2.0.1", "pydantic>=2.11.9", "pillow>=10.0.0", "pymupdf>=1.24.0", "black>=25.9.0", "pillow-heif>=1.1.1", "flask-jwt-extended>=4.7.1", "bcrypt>=5.0.0", "pony>=0.7.19", "flask-login>=0.6.3", "quart>=0.20.0", "tortoise-orm>=0.25.1", "quart-jwt-extended>=0.1.0", "pre-commit>=4.3.0", "tortoise-orm-stubs>=1.0.2", "aerich>=0.8.0", "tomlkit>=0.13.3", "authlib>=1.3.0", "asyncpg>=0.30.0"] +dependencies = [ + "chromadb>=1.1.0", + "python-dotenv>=1.0.0", + "flask>=3.1.2", + "httpx>=0.28.1", + "ollama>=0.6.0", + "openai>=2.0.1", + "pydantic>=2.11.9", + "pillow>=10.0.0", + "pymupdf>=1.24.0", + "black>=25.9.0", + "pillow-heif>=1.1.1", + "flask-jwt-extended>=4.7.1", + "bcrypt>=5.0.0", + "pony>=0.7.19", + "flask-login>=0.6.3", + "quart>=0.20.0", + "tortoise-orm>=0.25.1", + "quart-jwt-extended>=0.1.0", + "pre-commit>=4.3.0", + "tortoise-orm-stubs>=1.0.2", + "aerich>=0.8.0", + "tomlkit>=0.13.3", + "authlib>=1.3.0", + "asyncpg>=0.30.0", + "langchain-openai>=1.1.6", + "langchain>=1.2.0", +] [tool.aerich] tortoise_orm = "app.TORTOISE_CONFIG" diff --git a/services/raggr/raggr-frontend/Dockerfile.dev b/services/raggr/raggr-frontend/Dockerfile.dev index 37ad44b..35fa3d2 100644 --- a/services/raggr/raggr-frontend/Dockerfile.dev +++ b/services/raggr/raggr-frontend/Dockerfile.dev @@ -8,8 +8,11 @@ COPY package.json yarn.lock* ./ # Install dependencies RUN yarn install +# Copy application source code +COPY . . + # Expose rsbuild dev server port (default 3000) EXPOSE 3000 -# The actual source code will be mounted as a volume -# CMD will be specified in docker-compose +# Default command +CMD ["sh", "-c", "yarn build && yarn watch:build"] diff --git a/services/raggr/raggr-frontend/src/components/ChatScreen.tsx b/services/raggr/raggr-frontend/src/components/ChatScreen.tsx index 77fa284..58c4e36 100644 --- a/services/raggr/raggr-frontend/src/components/ChatScreen.tsx +++ b/services/raggr/raggr-frontend/src/components/ChatScreen.tsx @@ -80,6 +80,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { setConversations(parsedConversations); setSelectedConversation(parsedConversations[0]); console.log(parsedConversations); + console.log("JELLYFISH@"); } catch (error) { console.error("Failed to load messages:", error); } @@ -104,11 +105,18 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { useEffect(() => { const loadMessages = async () => { + console.log(selectedConversation); + console.log("JELLYFISH"); if (selectedConversation == null) return; try { const conversation = await conversationService.getConversation( selectedConversation.id, ); + // Update the conversation title in case it changed + setSelectedConversation({ + id: conversation.id, + title: conversation.name, + }); setMessages( conversation.messages.map((message) => ({ text: message.text, @@ -120,7 +128,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { } }; loadMessages(); - }, [selectedConversation]); + }, [selectedConversation?.id]); const handleQuestionSubmit = async () => { if (!query.trim()) return; // Don't submit empty messages @@ -180,9 +188,11 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { return (
{/* Sidebar - Expanded */} -