Muhammad-Mehdi commited on
Commit
29c052e
·
1 Parent(s): 9cee3e3

migrations fixed

Browse files
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: 0017_calendar_kit
4
- Revises: 0016_support_chat
5
  Create Date: 2026-10-06
6
  """
7
  import sqlalchemy as sa
8
  from alembic import op
9
 
10
- revision = "0017_calendar_kit"
11
- down_revision = "0016_support_chat"
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-*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing")]
 
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 unless the reader's instructions
455
- ask for one: those are offered to the reader separately as visuals they can add. Never write about visuals, mind maps, flashcards, quizzes, tables or timelines in the text, and never suggest or describe one, even if the reader's instructions mention them: the app offers those to the reader separately, so write only the report itself. Write in the language requested.
 
 
 
 
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". Then decide, section by section, where a visual would help the reader and which one. These are only
 
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 quiz questions, flashcards or
 
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, quizzes, tables or timelines in the text, and never suggest or describe one, even if the reader's instructions mention them: the app offers those to the reader separately, so write only the report itself. Write in the language requested. Never use em dashes."""
 
 
 
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
- child, _ = start_artifact(db, report.workspace_id, "infographic", title=f"Infographic: {brief[:60] or report.content_json.get('title', '')}",
 
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", "flashcards", "report", "infographic"]
 
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: Literal["summary", "audio_overview", "video", "quote_card", "carousel", "launch_kit", "mind_map", "quiz", "flashcards", "report", "infographic"]
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": {"audio_minutes": plan.audio_minutes, "videos": videos["limit"],
64
- "launch_kits": plan.launch_kits, "reports": plan.reports, "infographics": plan.infographics, "sources": plan.sources, "indexed_posts": plan.indexed_posts},
 
 
 
 
 
 
 
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(r"(?i)\b(?:fail\w*|slow|forever|too\s+long|taking\s+(?:so\s+|way\s+)?long|hang\w*|never\s+(?:finish|load|appear)\w*|error|broken|stuck|crash\w*|not\s+working|won'?t|doesn'?t\s+work|missing|lost)\b")
 
 
 
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": []}