Skip to content

Commit a291bc4

Browse files
committed
feat: rewrite report annotation as an interwoven blog post
Replace the overall-story-plus-notes annotation with a StoryAgent that returns ordered prose and view blocks, so the exported report reads like a blog post with narrative woven between the charts and tables. Clicking Annotate now opens a guidance popup (tone presets plus free text) to steer the angle, and a Report/Story toggle keeps the section export checkboxes reachable after a story is generated.
1 parent fb07ad0 commit a291bc4

4 files changed

Lines changed: 228 additions & 149 deletions

File tree

lumen/ai/agents/story.py

Lines changed: 37 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import param
44
import yaml
55

6-
from panel.pane import Markdown
7-
from panel_material_ui import ChatMessage, Typography
86
from pydantic import BaseModel, Field
97

108
from ...pipeline import Pipeline
@@ -17,36 +15,47 @@
1715
_MAX_DATA_CHARS = 2000
1816

1917

20-
class Story(BaseModel):
18+
class StoryBlock(BaseModel):
2119

22-
chain_of_thought: str = Field(
23-
description="In 1-2 sentences, outline the narrative arc across the selected tables and charts."
20+
prose: str = Field(
21+
default="",
22+
description=(
23+
"A narrative paragraph in Markdown. Leave empty when this block "
24+
"places a chart or table instead."
25+
),
2426
)
2527

26-
title: str = Field(
27-
description="A short, descriptive title for the report story."
28+
view: int | None = Field(
29+
default=None,
30+
description=(
31+
"The number of the chart or table to place here, taken from the "
32+
"catalog. Leave null for a prose block."
33+
),
2834
)
2935

30-
markdown: str = Field(
31-
description="The narrative in Markdown, referencing the selected tables and charts and their key findings."
32-
)
3336

34-
35-
class SectionNote(BaseModel):
37+
class Story(BaseModel):
3638

3739
chain_of_thought: str = Field(
38-
description="In 1-2 sentences, the key insight for this section."
40+
description="In 1-2 sentences, outline the narrative arc that ties the charts and tables together."
3941
)
4042

41-
markdown: str = Field(
42-
description="A concise Markdown note (2-4 sentences) annotating this section's table(s) and chart(s)."
43+
title: str = Field(
44+
description="A short, descriptive title for the report story."
45+
)
46+
47+
blocks: list[StoryBlock] = Field(
48+
description=(
49+
"The report as a flowing blog post: alternating prose blocks and "
50+
"chart/table blocks, in reading order."
51+
)
4352
)
4453

4554

4655
class StoryAgent(LLMUser):
4756
"""
48-
Writes a natural-language "story" annotating the tables and charts a user
49-
selected for export: one overall narrative plus per-section notes.
57+
Writes a "story" that reads like a blog post: short prose paragraphs
58+
interleaved with the charts and tables a user selected for export.
5059
"""
5160

5261
prompts = param.Dict(
@@ -55,33 +64,20 @@ class StoryAgent(LLMUser):
5564
"template": PROMPTS_DIR / "StoryAgent" / "main.jinja2",
5665
"response_model": Story,
5766
},
58-
"section": {
59-
"template": PROMPTS_DIR / "StoryAgent" / "section.jinja2",
60-
"response_model": SectionNote,
61-
},
6267
}
6368
)
6469

65-
async def write_story(self, report_context: str, title: str = "") -> Story:
66-
"""Write the overall narrative covering all selected sections."""
70+
async def write_story(self, catalog: str, guidance: str = "", title: str = "") -> Story:
71+
"""Write the blog-post story that weaves prose around the catalogued views."""
6772
return await self._invoke_prompt(
6873
"main",
6974
messages=[{"role": "user", "content": "Write the report story."}],
7075
context={},
71-
report_context=report_context,
76+
catalog=catalog,
77+
guidance=guidance,
7278
report_title=title,
7379
)
7480

75-
async def write_note(self, section_context: str, title: str = "") -> SectionNote:
76-
"""Write a short note annotating a single section."""
77-
return await self._invoke_prompt(
78-
"section",
79-
messages=[{"role": "user", "content": "Write the section note."}],
80-
context={},
81-
section_context=section_context,
82-
section_title=title,
83-
)
84-
8581

8682
async def _describe_editor(view: LumenEditor) -> str:
8783
"""Summarize a chart/table editor as spec + data summary for the prompt."""
@@ -92,7 +88,7 @@ async def _describe_editor(view: LumenEditor) -> str:
9288
parts.append(f"Specification:\n{spec[:_MAX_SPEC_CHARS]}")
9389
except (TypeError, ValueError, AttributeError, yaml.YAMLError):
9490
pass
95-
pipeline = component if isinstance(component, Pipeline) else getattr(component, "pipeline", None)
91+
pipeline = component if isinstance(component, Pipeline) else component.pipeline
9692
if pipeline is not None:
9793
try:
9894
data = await get_data(pipeline)
@@ -103,30 +99,10 @@ async def _describe_editor(view: LumenEditor) -> str:
10399
return "\n".join(parts) or (view.title or type(component).__name__)
104100

105101

106-
async def build_view_context(view) -> str | None:
107-
"""Describe a single report view (text, table, or chart) for the story prompt."""
108-
if isinstance(view, (Typography, Markdown)):
109-
return view.object
110-
if isinstance(view, str):
111-
return view
112-
if isinstance(view, ChatMessage):
113-
obj = view.object
114-
return obj.object if isinstance(obj, Markdown) else (obj if isinstance(obj, str) else None)
115-
if isinstance(view, LumenEditor):
116-
return await _describe_editor(view)
117-
return None
118-
119-
120-
async def build_section_context(section) -> str:
121-
"""Compact context describing one section's tables, charts, and notes."""
122-
described = [await build_view_context(view) for view in section.views]
123-
return "\n\n".join(text.strip() for text in described if text and text.strip())
124-
125-
126-
async def build_report_context(sections) -> str:
127-
"""Context describing every selected section for the overall story."""
128-
blocks = [
129-
f"## {section.title or 'Section'}\n{await build_section_context(section)}"
130-
for section in sections
131-
]
102+
async def build_catalog(views) -> str:
103+
"""Number each chart/table so the story can reference them by their catalog number."""
104+
blocks = []
105+
for i, view in enumerate(views, start=1):
106+
label = view.title or f"View {i}"
107+
blocks.append(f"### View {i}: {label}\n{await _describe_editor(view)}")
132108
return "\n\n".join(blocks)
Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
{% extends 'Actor/main.jinja2' %}
22

33
{%- block instructions -%}
4-
You are a data storyteller. Write a cohesive narrative about a report made up of the
5-
selected tables and charts below.
4+
You are a data storyteller. Turn the catalogued charts and tables below into a
5+
report that reads like a blog post, weaving prose around the visuals.
66

7-
- Open with a short overview of what the report covers.
8-
- Walk through the key findings, referring to the tables and charts by their section titles.
9-
- Surface notable patterns, comparisons, and takeaways grounded ONLY in the provided data
10-
summaries and chart specifications.
11-
- Do not invent numbers that are not supported by the data summaries.
12-
- Write in Markdown and keep it tight and readable.
7+
- Return an ordered list of blocks. Each block is either prose or a chart/table.
8+
- A prose block has `prose` set to Markdown text and `view` left null.
9+
- A chart/table block has `view` set to that view's number from the catalog and
10+
`prose` left empty.
11+
- Open with a short prose block introducing what the report covers, then walk
12+
through the catalog: a prose lead-in, the chart/table it discusses, and so on,
13+
so pictures and text alternate like a blog post.
14+
- Reference every catalogued view exactly once and only use numbers that appear
15+
in the catalog.
16+
- Ground every claim ONLY in the provided data summaries and specifications; do
17+
not invent numbers.
18+
{%- if guidance %}
19+
- Follow this guidance on angle and tone: {{ guidance }}
20+
{%- endif %}
1321
{%- endblock -%}
1422

1523
{%- block context -%}
1624
Report title: {{ report_title }}
1725

18-
Selected tables and charts:
19-
{{ report_context }}
26+
Catalog of charts and tables:
27+
{{ catalog }}
2028
{%- endblock -%}

lumen/ai/prompts/StoryAgent/section.jinja2

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)