Spaces:
Runtime error
Runtime error
Commit ·
29c052e
1
Parent(s): 9cee3e3
migrations fixed
Browse files- backend/alembic/versions/{0017_calendar_kit.py → 0020_calendar_kit.py} +4 -4
- backend/app/infographics/image.py +2 -1
- backend/app/infographics/render.py +1 -1
- backend/app/llm/signatures.py +14 -5
- backend/app/pipeline/report.py +4 -3
- backend/app/routers/artifacts.py +5 -4
- backend/app/routers/reports.py +1 -1
- backend/app/services/usage.py +9 -2
- backend/app/support/scope.py +4 -1
- backend/tests/test_buildout.py +0 -1
backend/alembic/versions/{0017_calendar_kit.py → 0020_calendar_kit.py}
RENAMED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
"""calendar_items.kit_id: the Launch Kit a scheduled post was written from, kept apart from its attachment
|
| 2 |
|
| 3 |
-
Revision ID:
|
| 4 |
-
Revises:
|
| 5 |
Create Date: 2026-10-06
|
| 6 |
"""
|
| 7 |
import sqlalchemy as sa
|
| 8 |
from alembic import op
|
| 9 |
|
| 10 |
-
revision = "
|
| 11 |
-
down_revision = "
|
| 12 |
branch_labels = None
|
| 13 |
depends_on = None
|
| 14 |
|
|
|
|
| 1 |
"""calendar_items.kit_id: the Launch Kit a scheduled post was written from, kept apart from its attachment
|
| 2 |
|
| 3 |
+
Revision ID: 0020_calendar_kit
|
| 4 |
+
Revises: 0019_infographics_limit
|
| 5 |
Create Date: 2026-10-06
|
| 6 |
"""
|
| 7 |
import sqlalchemy as sa
|
| 8 |
from alembic import op
|
| 9 |
|
| 10 |
+
revision = "0020_calendar_kit"
|
| 11 |
+
down_revision = "0019_infographics_limit"
|
| 12 |
branch_labels = None
|
| 13 |
depends_on = None
|
| 14 |
|
backend/app/infographics/image.py
CHANGED
|
@@ -32,7 +32,8 @@ def chrome() -> str | None:
|
|
| 32 |
if found := shutil.which(name):
|
| 33 |
return found
|
| 34 |
patterns = ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
| 35 |
-
os.path.expanduser("~/.cache/puppeteer/chrome/*/chrome-mac-*/
|
|
|
|
| 36 |
return next((p for pat in patterns for p in glob.glob(pat)), None)
|
| 37 |
|
| 38 |
|
|
|
|
| 32 |
if found := shutil.which(name):
|
| 33 |
return found
|
| 34 |
patterns = ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
| 35 |
+
os.path.expanduser("~/.cache/puppeteer/chrome/*/chrome-mac-*/"
|
| 36 |
+
"Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing")]
|
| 37 |
return next((p for pat in patterns for p in glob.glob(pat)), None)
|
| 38 |
|
| 39 |
|
backend/app/infographics/render.py
CHANGED
|
@@ -5,8 +5,8 @@ import hashlib
|
|
| 5 |
import random
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
-
from markupsafe import Markup
|
| 9 |
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
|
|
|
|
| 10 |
|
| 11 |
from app.infographics.content import HEIGHT, WIDTH
|
| 12 |
from app.infographics.themes import DEFAULT_THEME, THEMES, theme_id
|
|
|
|
| 5 |
import random
|
| 6 |
from pathlib import Path
|
| 7 |
|
|
|
|
| 8 |
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
|
| 9 |
+
from markupsafe import Markup
|
| 10 |
|
| 11 |
from app.infographics.content import HEIGHT, WIDTH
|
| 12 |
from app.infographics.themes import DEFAULT_THEME, THEMES, theme_id
|
backend/app/llm/signatures.py
CHANGED
|
@@ -451,8 +451,12 @@ class WriteReport(dspy.Signature):
|
|
| 451 |
"Section 2" or "Details"), and they read in a logical order. Use "### " inside sections, lists, and tables where
|
| 452 |
they help. When the material has numbered lines (FILE blocks), put a [n] marker after each
|
| 453 |
claim it supports and list each n in citations with the path and lines it came from; when it is a chat transcript,
|
| 454 |
-
write no markers and no citations. Do not write a quiz, flashcards, practice or review questions into the report
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
Never use em dashes."""
|
| 457 |
|
| 458 |
title: str = dspy.InputField(desc="The notebook, post or chat the material comes from")
|
|
@@ -483,7 +487,8 @@ class PlanInteractiveReport(dspy.Signature):
|
|
| 483 |
section that is itself a quiz, flashcards, practice questions or a mind map, because those are visuals, and are
|
| 484 |
suggested through a section's visual instead. Choose 4 to 8 sections in a sensible
|
| 485 |
order, starting with an Introduction and ending with a Summary unless the reader asks for something else. Each
|
| 486 |
-
heading is specific and descriptive of what the section covers, never generic like "Details" or "Section 2".
|
|
|
|
| 487 |
suggestions: nothing is built until the reader clicks Add, so suggest the spots where each really fits: a mind_map to show how
|
| 488 |
many linked ideas fit together (at most one, usually near the start or the end), flashcards to learn a set of
|
| 489 |
terms or facts, a quiz to check understanding after a dense section, an infographic for a one page picture of the
|
|
@@ -502,11 +507,15 @@ class PlanInteractiveReport(dspy.Signature):
|
|
| 502 |
|
| 503 |
class WriteReportSection(dspy.Signature):
|
| 504 |
"""Write one section of a report, using only the material given. Follow the reader's instructions on style, tone
|
| 505 |
-
and length; a section is two to five short paragraphs, or a list or table where that reads better. Do not write
|
|
|
|
| 506 |
review questions in the section (those are offered to the reader separately). Do not repeat
|
| 507 |
the heading, and do not use "#" or "##" headings; "###" subheadings are fine. When the material has numbered lines (FILE
|
| 508 |
blocks), put a [n] marker after each claim it supports and list each n in citations with the path and lines; when it
|
| 509 |
-
is a chat transcript, write no markers and no citations. Never write about visuals, mind maps, flashcards,
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
title: str = dspy.InputField()
|
| 512 |
report_title: str = dspy.InputField()
|
|
|
|
| 451 |
"Section 2" or "Details"), and they read in a logical order. Use "### " inside sections, lists, and tables where
|
| 452 |
they help. When the material has numbered lines (FILE blocks), put a [n] marker after each
|
| 453 |
claim it supports and list each n in citations with the path and lines it came from; when it is a chat transcript,
|
| 454 |
+
write no markers and no citations. Do not write a quiz, flashcards, practice or review questions into the report
|
| 455 |
+
unless the reader's instructions
|
| 456 |
+
ask for one: those are offered to the reader separately as visuals they can add. Never write about visuals, mind
|
| 457 |
+
maps, flashcards, quizzes, tables or timelines in the text, and never suggest or describe one, even if the
|
| 458 |
+
reader's instructions mention them: the app offers those to the reader separately, so write only the report
|
| 459 |
+
itself. Write in the language requested.
|
| 460 |
Never use em dashes."""
|
| 461 |
|
| 462 |
title: str = dspy.InputField(desc="The notebook, post or chat the material comes from")
|
|
|
|
| 487 |
section that is itself a quiz, flashcards, practice questions or a mind map, because those are visuals, and are
|
| 488 |
suggested through a section's visual instead. Choose 4 to 8 sections in a sensible
|
| 489 |
order, starting with an Introduction and ending with a Summary unless the reader asks for something else. Each
|
| 490 |
+
heading is specific and descriptive of what the section covers, never generic like "Details" or "Section 2".
|
| 491 |
+
Then decide, section by section, where a visual would help the reader and which one. These are only
|
| 492 |
suggestions: nothing is built until the reader clicks Add, so suggest the spots where each really fits: a mind_map to show how
|
| 493 |
many linked ideas fit together (at most one, usually near the start or the end), flashcards to learn a set of
|
| 494 |
terms or facts, a quiz to check understanding after a dense section, an infographic for a one page picture of the
|
|
|
|
| 507 |
|
| 508 |
class WriteReportSection(dspy.Signature):
|
| 509 |
"""Write one section of a report, using only the material given. Follow the reader's instructions on style, tone
|
| 510 |
+
and length; a section is two to five short paragraphs, or a list or table where that reads better. Do not write
|
| 511 |
+
quiz questions, flashcards or
|
| 512 |
review questions in the section (those are offered to the reader separately). Do not repeat
|
| 513 |
the heading, and do not use "#" or "##" headings; "###" subheadings are fine. When the material has numbered lines (FILE
|
| 514 |
blocks), put a [n] marker after each claim it supports and list each n in citations with the path and lines; when it
|
| 515 |
+
is a chat transcript, write no markers and no citations. Never write about visuals, mind maps, flashcards,
|
| 516 |
+
quizzes, tables or timelines in the text, and never suggest or describe one, even if the reader's instructions
|
| 517 |
+
mention them: the app offers those to the reader separately, so write only the report itself. Write in the
|
| 518 |
+
language requested. Never use em dashes."""
|
| 519 |
|
| 520 |
title: str = dspy.InputField()
|
| 521 |
report_title: str = dspy.InputField()
|
backend/app/pipeline/report.py
CHANGED
|
@@ -10,6 +10,7 @@ import uuid
|
|
| 10 |
|
| 11 |
from sqlalchemy.orm import Session
|
| 12 |
|
|
|
|
| 13 |
from app.llm import run
|
| 14 |
from app.llm.provider import fast_lm, report_lm
|
| 15 |
from app.llm.signatures import (
|
|
@@ -22,10 +23,9 @@ from app.models import Artifact, Job
|
|
| 22 |
from app.pipeline.flashcards import make_cards
|
| 23 |
from app.pipeline.generate import NothingToDo, mind_map_content
|
| 24 |
from app.pipeline.material import Material, load_material
|
| 25 |
-
from app.pipeline.quiz import make_questions
|
| 26 |
from app.pipeline.passages import as_citations
|
|
|
|
| 27 |
from app.pipeline.research import verify_citations
|
| 28 |
-
from app.infographics.themes import theme_id
|
| 29 |
from app.services.artifacts import start_artifact
|
| 30 |
from app.services.jobs import update_job
|
| 31 |
from app.services.renderer import PermanentJobError
|
|
@@ -319,7 +319,8 @@ def make_block(db: Session, job: Job, report: Artifact, kind: str, brief: str, t
|
|
| 319 |
if kind == "infographic":
|
| 320 |
# A picture is drawn by the renderer, so it is its own infographic (listed in the Library, counted in the
|
| 321 |
# plan) that the block points at; the report page shows it once it is ready.
|
| 322 |
-
|
|
|
|
| 323 |
notebook_id=report.notebook_id,
|
| 324 |
params={**_source_params(source), "theme": theme_id(theme), "instructions": brief[:600]})
|
| 325 |
return {"title": f"Infographic: {brief[:60]}", "artifact_id": str(child.id), "theme": theme_id(theme)}
|
|
|
|
| 10 |
|
| 11 |
from sqlalchemy.orm import Session
|
| 12 |
|
| 13 |
+
from app.infographics.themes import theme_id
|
| 14 |
from app.llm import run
|
| 15 |
from app.llm.provider import fast_lm, report_lm
|
| 16 |
from app.llm.signatures import (
|
|
|
|
| 23 |
from app.pipeline.flashcards import make_cards
|
| 24 |
from app.pipeline.generate import NothingToDo, mind_map_content
|
| 25 |
from app.pipeline.material import Material, load_material
|
|
|
|
| 26 |
from app.pipeline.passages import as_citations
|
| 27 |
+
from app.pipeline.quiz import make_questions
|
| 28 |
from app.pipeline.research import verify_citations
|
|
|
|
| 29 |
from app.services.artifacts import start_artifact
|
| 30 |
from app.services.jobs import update_job
|
| 31 |
from app.services.renderer import PermanentJobError
|
|
|
|
| 319 |
if kind == "infographic":
|
| 320 |
# A picture is drawn by the renderer, so it is its own infographic (listed in the Library, counted in the
|
| 321 |
# plan) that the block points at; the report page shows it once it is ready.
|
| 322 |
+
title = f"Infographic: {brief[:60] or report.content_json.get('title', '')}"
|
| 323 |
+
child, _ = start_artifact(db, report.workspace_id, "infographic", title=title,
|
| 324 |
notebook_id=report.notebook_id,
|
| 325 |
params={**_source_params(source), "theme": theme_id(theme), "instructions": brief[:600]})
|
| 326 |
return {"title": f"Infographic: {brief[:60]}", "artifact_id": str(child.id), "theme": theme_id(theme)}
|
backend/app/routers/artifacts.py
CHANGED
|
@@ -8,20 +8,21 @@ from sqlalchemy import func, or_, select
|
|
| 8 |
|
| 9 |
from app.auth import Ctx, get_ctx
|
| 10 |
from app.config import settings
|
|
|
|
|
|
|
| 11 |
from app.models import Artifact, CalendarItem, Chat, Document, Job, Notebook, UserSavedVoice
|
| 12 |
from app.routers.notebooks import ensure_archive_notebook
|
| 13 |
-
from app.infographics.image import ImageUnavailable, render_png
|
| 14 |
from app.services.artifacts import latest_jobs, serialize_artifact, start_artifact
|
| 15 |
from app.services.jobs import create_job, serialize_job
|
| 16 |
from app.services.plans import effective_plan, plan_limit_error
|
| 17 |
-
from app.infographics.themes import theme_id
|
| 18 |
from app.services.renderer import COMPOSITIONS
|
| 19 |
from app.services.storage import storage
|
| 20 |
from app.services.usage import check_limit
|
| 21 |
|
| 22 |
router = APIRouter(prefix="/api/artifacts", tags=["artifacts"])
|
| 23 |
|
| 24 |
-
ArtifactType = Literal["summary", "audio_overview", "video", "quote_card", "carousel", "launch_kit", "mind_map", "quiz",
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
SOURCE_TYPES = ("quiz", "flashcards", "report", "infographic") # made from posts or chats picked in the dialog
|
|
@@ -33,7 +34,7 @@ class RenderIn(BaseModel):
|
|
| 33 |
|
| 34 |
|
| 35 |
class GenerateIn(BaseModel):
|
| 36 |
-
type:
|
| 37 |
notebook_id: uuid.UUID | None = None
|
| 38 |
document_id: uuid.UUID | None = None
|
| 39 |
archive: bool = False # no notebook or post picked: use the "All posts" notebook
|
|
|
|
| 8 |
|
| 9 |
from app.auth import Ctx, get_ctx
|
| 10 |
from app.config import settings
|
| 11 |
+
from app.infographics.image import ImageUnavailable, render_png
|
| 12 |
+
from app.infographics.themes import theme_id
|
| 13 |
from app.models import Artifact, CalendarItem, Chat, Document, Job, Notebook, UserSavedVoice
|
| 14 |
from app.routers.notebooks import ensure_archive_notebook
|
|
|
|
| 15 |
from app.services.artifacts import latest_jobs, serialize_artifact, start_artifact
|
| 16 |
from app.services.jobs import create_job, serialize_job
|
| 17 |
from app.services.plans import effective_plan, plan_limit_error
|
|
|
|
| 18 |
from app.services.renderer import COMPOSITIONS
|
| 19 |
from app.services.storage import storage
|
| 20 |
from app.services.usage import check_limit
|
| 21 |
|
| 22 |
router = APIRouter(prefix="/api/artifacts", tags=["artifacts"])
|
| 23 |
|
| 24 |
+
ArtifactType = Literal["summary", "audio_overview", "video", "quote_card", "carousel", "launch_kit", "mind_map", "quiz",
|
| 25 |
+
"flashcards", "report", "infographic"]
|
| 26 |
|
| 27 |
|
| 28 |
SOURCE_TYPES = ("quiz", "flashcards", "report", "infographic") # made from posts or chats picked in the dialog
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
class GenerateIn(BaseModel):
|
| 37 |
+
type: ArtifactType
|
| 38 |
notebook_id: uuid.UUID | None = None
|
| 39 |
document_id: uuid.UUID | None = None
|
| 40 |
archive: bool = False # no notebook or post picked: use the "All posts" notebook
|
backend/app/routers/reports.py
CHANGED
|
@@ -15,8 +15,8 @@ from app.routers.artifacts import get_artifact_or_404
|
|
| 15 |
from app.routers.videos import FocusIn, _focus_material
|
| 16 |
from app.services.artifacts import latest_jobs, serialize_artifact
|
| 17 |
from app.services.jobs import create_job
|
| 18 |
-
from app.services.usage import check_limit
|
| 19 |
from app.services.report_templates import INTERACTIVE, TEMPLATES
|
|
|
|
| 20 |
|
| 21 |
router = APIRouter(prefix="/api/reports", tags=["reports"])
|
| 22 |
|
|
|
|
| 15 |
from app.routers.videos import FocusIn, _focus_material
|
| 16 |
from app.services.artifacts import latest_jobs, serialize_artifact
|
| 17 |
from app.services.jobs import create_job
|
|
|
|
| 18 |
from app.services.report_templates import INTERACTIVE, TEMPLATES
|
| 19 |
+
from app.services.usage import check_limit
|
| 20 |
|
| 21 |
router = APIRouter(prefix="/api/reports", tags=["reports"])
|
| 22 |
|
backend/app/services/usage.py
CHANGED
|
@@ -60,8 +60,15 @@ def usage_report(db: Session, workspace: Workspace) -> dict:
|
|
| 60 |
"plan": plan.id,
|
| 61 |
"used": used,
|
| 62 |
"videos_resets_at": videos["resets_at"],
|
| 63 |
-
"limits": {
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
|
|
|
|
| 60 |
"plan": plan.id,
|
| 61 |
"used": used,
|
| 62 |
"videos_resets_at": videos["resets_at"],
|
| 63 |
+
"limits": {
|
| 64 |
+
"audio_minutes": plan.audio_minutes,
|
| 65 |
+
"videos": videos["limit"],
|
| 66 |
+
"launch_kits": plan.launch_kits,
|
| 67 |
+
"reports": plan.reports,
|
| 68 |
+
"infographics": plan.infographics,
|
| 69 |
+
"sources": plan.sources,
|
| 70 |
+
"indexed_posts": plan.indexed_posts,
|
| 71 |
+
},
|
| 72 |
}
|
| 73 |
|
| 74 |
|
backend/app/support/scope.py
CHANGED
|
@@ -47,7 +47,10 @@ _ABOUT_MY_DATA = re.compile(
|
|
| 47 |
"""
|
| 48 |
)
|
| 49 |
# Words that mean something went wrong with their work; the reply also offers the human form.
|
| 50 |
-
_BROKEN = re.compile(
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
OUT_OF_SCOPE_REPLY = (
|
| 53 |
"I can help with how Notestack works, but I can't see your notebooks, posts, files or account, so I can't answer "
|
|
|
|
| 47 |
"""
|
| 48 |
)
|
| 49 |
# Words that mean something went wrong with their work; the reply also offers the human form.
|
| 50 |
+
_BROKEN = re.compile(
|
| 51 |
+
r"(?i)\b(?:fail\w*|slow|forever|too\s+long|taking\s+(?:so\s+|way\s+)?long|hang\w*"
|
| 52 |
+
r"|never\s+(?:finish|load|appear)\w*|error|broken|stuck|crash\w*|not\s+working|won'?t|doesn'?t\s+work|missing|lost)\b"
|
| 53 |
+
)
|
| 54 |
|
| 55 |
OUT_OF_SCOPE_REPLY = (
|
| 56 |
"I can help with how Notestack works, but I can't see your notebooks, posts, files or account, so I can't answer "
|
backend/tests/test_buildout.py
CHANGED
|
@@ -1822,7 +1822,6 @@ def test_infographic_is_a_page_at_once_and_can_be_shared_by_link(client, auth, r
|
|
| 1822 |
|
| 1823 |
|
| 1824 |
def test_infographic_needs_a_source_and_enough_text_and_obeys_its_limit(client, auth, run_jobs, feed, llm, monkeypatch):
|
| 1825 |
-
from app.pipeline import infographic
|
| 1826 |
|
| 1827 |
pricing, nb = _notebook_with_pricing(client, auth, run_jobs)
|
| 1828 |
llm["ExtractIdeas"] = {"ideas": []}
|
|
|
|
| 1822 |
|
| 1823 |
|
| 1824 |
def test_infographic_needs_a_source_and_enough_text_and_obeys_its_limit(client, auth, run_jobs, feed, llm, monkeypatch):
|
|
|
|
| 1825 |
|
| 1826 |
pricing, nb = _notebook_with_pricing(client, auth, run_jobs)
|
| 1827 |
llm["ExtractIdeas"] = {"ideas": []}
|