Skip to content
Open
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
7 changes: 4 additions & 3 deletions apps/projects/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
)
from apps.summarization.export_utils.core import generate_full_export
from apps.summarization.pydantic_models import ProjectSummaryResponse
from apps.summarization.services import AIService
from apps.summarization.services import DocumentProcessor
from apps.summarization.services import ProjectSummarizer

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -44,7 +45,7 @@ def generate_project_summary(
)
if documents_dict:
try:
service = AIService()
service = DocumentProcessor()
document_response = service.request_vision_dict(
documents_dict=documents_dict
)
Expand All @@ -64,7 +65,7 @@ def generate_project_summary(

json_text = json.dumps(export_data, indent=2)
prompt = Settings.get_value("project_summary_prompt")
service = AIService()
service = ProjectSummarizer()
response = service.project_summarize(
project=project,
text=json_text,
Expand Down
25 changes: 25 additions & 0 deletions apps/summarization/pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,31 @@ def is_document(self) -> bool:
return any(url_lower.endswith(ext) for ext in document_extensions)


# Add these new models for module-by-module summarization
class ModuleSummaryResponse(BaseModel):
"""Response model for a single module summary."""

summary: str = Field(
description="A 2-3 sentence overview of what happened in this module"
)
bullets: List[str] = Field(
default_factory=list,
description="Key points about contributions, ideas, or outcomes",
)


class GeneralInfoResponse(BaseModel):
"""Response model for project general information."""

summary: str = Field(
description="A 3-4 sentence overview of the entire project's participation journey"
)
goals: List[str] = Field(
default_factory=list,
description="Main goals or themes that emerged from the participation",
)


class DocumentSummaryItem(BaseModel):
"""Response model for a single document summary with handle."""

Expand Down
13 changes: 13 additions & 0 deletions apps/summarization/requests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""AI requests package."""

from .base import AIRequest
from .document import DocumentRequest
from .document import MultimodalSummaryRequest
from .project import SummaryRequest

__all__ = [
"AIRequest",
"SummaryRequest",
"MultimodalSummaryRequest",
"DocumentRequest",
]
15 changes: 15 additions & 0 deletions apps/summarization/requests/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Base classes for AI requests."""

from abc import ABC
from abc import abstractmethod


class AIRequest(ABC):
"""Base class for all AI requests."""

vision_support = False

@abstractmethod
def prompt(self) -> str:
"""Return the prompt text for this request."""
pass
37 changes: 37 additions & 0 deletions apps/summarization/requests/document.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Document and image processing requests."""

from .base import AIRequest


class MultimodalSummaryRequest(AIRequest):
"""Request model for multimodal document summarization."""

vision_support = True
PROMPT = "Summarize this image/document. Return JSON with summary field."

def __init__(
self, image_urls: list[str], text: str | None = None, prompt: str | None = None
):
super().__init__()
self.image_urls = image_urls
self.prompt_text = prompt or self.PROMPT
self.text = text

def prompt(self) -> str:
base = self.prompt_text
return f"{base}\n\nText:\n{self.text}" if self.text else base


class DocumentRequest(AIRequest):
"""Request model for document summarization."""

vision_support = True
PROMPT = "Summarize this document. Return JSON with summary field."

def __init__(self, url: str, prompt: str | None = None):
super().__init__()
self.image_urls = [url]
self.prompt_text = prompt or self.PROMPT

def prompt(self) -> str:
return self.prompt_text
121 changes: 121 additions & 0 deletions apps/summarization/requests/project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Project-specific AI requests."""

import json
from typing import Any
from typing import Dict
from typing import List

from .base import AIRequest


class ModuleSummaryRequest(AIRequest):
"""Request for summarizing a single module."""

PROMPT_TEMPLATE = """
Summarize this participation module:

Module: {module_name}
Phase: {phase}
Description: {description}
Data: {content}

Return ONLY valid JSON with EXACTLY this format:
{{
"summary": "A 2-3 sentence overview of what happened in this module",
"bullets": [
"Key point 1 about specific contributions",
"Key point 2 about ideas or proposals",
"Key point 3 about engagement or outcomes"
]
}}

The response MUST include BOTH "summary" and "bullets" fields.
"bullets" MUST be an array of strings, never empty.
"""

def __init__(self, module_data: Dict[str, Any], phase: str):
super().__init__()
self.module_data = module_data
self.phase = phase

def prompt(self) -> str:
"""Generate the prompt for this module."""
return self.PROMPT_TEMPLATE.format(
module_name=self.module_data.get("module_name", "Unknown"),
phase=self.phase,
description=self.module_data.get("description", "No description"),
content=json.dumps(self.module_data.get("content", {})),
)


class GeneralInfoRequest(AIRequest):
"""Request for project-level summary."""

PROMPT_TEMPLATE = """
Summarize this entire project:

Project: {project_name}
Description: {description}
Module Summaries: {module_summaries}

Return ONLY valid JSON with EXACTLY this format:
{{
"summary": "A 3-4 sentence overview of the entire project's participation",
"goals": [
"First main goal or theme",
"Second main goal or theme",
"Third main goal or theme"
]
}}

The response MUST include BOTH "summary" and "goals" fields.
"goals" MUST be an array of strings, at least 2-3 items.
"""

def __init__(
self, project_data: Dict[str, Any], module_summaries: List[Dict[str, Any]]
):
super().__init__()
self.project_data = project_data
self.module_summaries = module_summaries

def prompt(self) -> str:
"""Generate the prompt for project summary."""
project = self.project_data.get("project", {})
return self.PROMPT_TEMPLATE.format(
project_name=project.get("name", "Unknown"),
description=project.get("information", "No description"),
module_summaries=json.dumps(self.module_summaries),
)


class SummaryRequest(AIRequest):
"""Legacy request model for text summarization."""

DEFAULT_PROMPT = """
You are a JSON generator. Return ONLY valid JSON.

Schema:
{
"title": "Summary of participation",
"general_info": {"summary": "string", "goals": ["string"]},
"phases": {
"past": {"modules": [{"module_id": "number", "module_name": "string", "status": "past", "final": {"summary": "string", "bullets": ["string"]}}]},
"current": {"modules": [{"module_id": "number", "module_name": "string", "status": "current", "final": {"summary": "string", "bullets": ["string"]}}]},
"upcoming": {"modules": [{"module_id": "number", "module_name": "string", "status": "upcoming", "final": {"summary": "string", "bullets": ["string"]}}]}
}
}

NOTE: module_id in the output should match the given module_id of the input for each module

Extract real data from the project export.
Respond with ONLY the JSON object.
"""

def __init__(self, text: str, prompt: str | None = None):
super().__init__()
self.text = text
self.prompt_text = prompt or self.DEFAULT_PROMPT

def prompt(self) -> str:
return f"{self.prompt_text}\n\n{self.text}"
Loading
Loading