Skip to content

Commit 6c01e04

Browse files
committed
test: cover the interwoven story, guidance popup, and view toggle
Rework the story tests for the block model and add coverage for the guidance dialog presets, the Report/Story toggle, and story reset.
1 parent a291bc4 commit 6c01e04

1 file changed

Lines changed: 179 additions & 84 deletions

File tree

lumen/tests/ai/test_story.py

Lines changed: 179 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -29,154 +29,249 @@ async def _execute(self, context, **kwargs):
2929
return [Markdown("B done")], {'text': 'B'}
3030

3131

32-
async def test_story_agent_write_story(llm):
33-
from lumen.ai.agents.story import Story, StoryAgent
32+
class ChartAction(Action):
33+
"""Emits a pipeline-backed LumenEditor so the story catalog has real views."""
3434

35-
llm.set_responses([Story(chain_of_thought="arc", title="Olympics", markdown="# Olympics\nBody")])
36-
agent = StoryAgent(llm=llm)
35+
source = param.Parameter()
3736

38-
result = await agent.write_story("Section: Medals\n<data summary>", title="Olympic Report")
37+
label = param.String(default="Chart")
3938

40-
assert isinstance(result, Story)
41-
assert result.title == "Olympics"
42-
assert result.markdown == "# Olympics\nBody"
39+
async def _execute(self, context, **kwargs):
40+
from lumen.ai.editors import LumenEditor
41+
from lumen.pipeline import Pipeline
4342

43+
pipeline = Pipeline(source=self.source, table='tiny')
44+
return [LumenEditor(component=pipeline, title=self.label)], {'text': self.label}
4445

45-
async def test_story_agent_write_note(llm):
46-
from lumen.ai.agents.story import SectionNote, StoryAgent
4746

48-
llm.set_responses([SectionNote(chain_of_thought="key", markdown="Men-only sports dominate.")])
49-
agent = StoryAgent(llm=llm)
47+
def _nb_text(report):
48+
return "".join(
49+
"".join(cell["source"]) for cell in json.loads(report.to_notebook())["cells"]
50+
)
5051

51-
result = await agent.write_note("Section: Gender\n<data summary>", title="Gender Exclusive")
5252

53-
assert isinstance(result, SectionNote)
54-
assert "Men-only" in result.markdown
53+
def _kinds(report):
54+
return [kind for kind, _ in report._story_blocks]
5555

5656

57-
async def test_build_report_context_includes_titles_and_text():
58-
from lumen.ai.agents.story import (
59-
build_report_context, build_section_context,
60-
)
57+
async def test_story_agent_write_story(llm):
58+
from lumen.ai.agents.story import Story, StoryAgent, StoryBlock
6159

62-
report = Report(
63-
Section(A(order=[]), title='Section A'),
64-
Section(B(order=[]), title='Section B'),
65-
title='My Report',
66-
)
67-
await report.execute()
60+
llm.set_responses([Story(
61+
chain_of_thought="arc", title="Olympics",
62+
blocks=[StoryBlock(prose="Intro."), StoryBlock(view=1), StoryBlock(prose="Wrap up.")],
63+
)])
64+
agent = StoryAgent(llm=llm)
6865

69-
ctx = await build_report_context([s for s in report if s.include_in_export])
70-
assert "Section A" in ctx
71-
assert "A done" in ctx
72-
assert "Section B" in ctx
73-
assert "B done" in ctx
66+
result = await agent.write_story(
67+
"### View 1: Medals\n<summary>", guidance="focus on trends", title="Olympic Report"
68+
)
7469

75-
section_ctx = await build_section_context(report[0])
76-
assert "A done" in section_ctx
77-
assert "B done" not in section_ctx
70+
assert isinstance(result, Story)
71+
assert result.title == "Olympics"
72+
assert [block.view for block in result.blocks] == [None, 1, None]
7873

7974

80-
async def test_build_section_context_summarizes_table_pipeline(tiny_source):
81-
from lumen.ai.agents.story import build_view_context
75+
async def test_build_catalog_numbers_and_summarizes(tiny_source):
76+
from lumen.ai.agents.story import build_catalog
8277
from lumen.ai.editors import LumenEditor
8378
from lumen.pipeline import Pipeline
8479

85-
pipeline = Pipeline(source=tiny_source, table='tiny')
86-
editor = LumenEditor(component=pipeline, title='Tiny table')
80+
editor = LumenEditor(component=Pipeline(source=tiny_source, table='tiny'), title='Tiny table')
8781

88-
ctx = await build_view_context(editor)
82+
ctx = await build_catalog([editor])
8983

90-
# The table's columns should surface in the summary sent to the LLM.
84+
# Views are numbered so the story can reference them, and the data surfaces.
85+
assert "View 1" in ctx
9186
assert "category" in ctx
9287
assert "value" in ctx
9388

9489

95-
def _nb_text(report):
96-
return "".join(
97-
"".join(cell["source"]) for cell in json.loads(report.to_notebook())["cells"]
98-
)
99-
100-
101-
async def test_report_annotate_inserts_story_and_notes(llm):
102-
from lumen.ai.agents.story import SectionNote, Story
90+
async def test_report_annotate_interleaves_prose_and_view(llm, tiny_source):
91+
from lumen.ai.agents.story import Story, StoryBlock
10392

10493
report = Report(
105-
Section(A(order=[]), title='Section A'),
106-
Section(B(order=[]), title='Section B'),
94+
Section(ChartAction(source=tiny_source, label='Chart A'), title='Section A'),
10795
title='My Report',
10896
llm=llm,
10997
)
11098
await report.execute()
111-
llm.set_responses([
112-
Story(chain_of_thought="c", title="The Big Picture", markdown="Overall narrative."),
113-
SectionNote(chain_of_thought="c", markdown="Note for A."),
114-
SectionNote(chain_of_thought="c", markdown="Note for B."),
115-
])
99+
llm.set_responses([Story(chain_of_thought="c", title="The Big Picture", blocks=[
100+
StoryBlock(prose="Lead-in prose."),
101+
StoryBlock(view=1),
102+
StoryBlock(prose="Closing prose."),
103+
])])
116104

117105
await report._annotate_report()
118106

119107
nb = _nb_text(report)
120108
assert "The Big Picture" in nb
121-
assert "Overall narrative." in nb
122-
assert "Note for A." in nb
123-
assert "Note for B." in nb
109+
# Prose is interleaved around the chart's reconstructed spec, blog-post style.
110+
assert nb.index("Lead-in prose.") < nb.index("lm.Pipeline.from_spec") < nb.index("Closing prose.")
124111

125-
# The overall story and the notes must both survive HTML export too.
112+
# The flow must survive HTML export too.
126113
html = report.to_html()
127114
assert "The Big Picture" in html
128-
assert "Overall narrative." in html
129-
assert "Note for A." in html
130-
assert "Note for B." in html
115+
assert "Lead-in prose." in html
116+
assert "Closing prose." in html
131117

132118

133-
async def test_report_annotate_only_selected_sections(llm):
134-
from lumen.ai.agents.story import SectionNote, Story
119+
async def test_report_annotate_appends_unreferenced_views(llm, tiny_source):
120+
from lumen.ai.agents.story import Story, StoryBlock
135121

136122
report = Report(
137-
Section(A(order=[]), title='Section A'),
138-
Section(B(order=[]), title='Section B'),
139-
title='My Report',
123+
Section(ChartAction(source=tiny_source, label='Chart A'), title='Section A'),
124+
Section(ChartAction(source=tiny_source, label='Chart B'), title='Section B'),
125+
title='R',
126+
llm=llm,
127+
)
128+
await report.execute()
129+
# The story only references the second chart; the first must not be dropped.
130+
llm.set_responses([Story(chain_of_thought="c", title="S", blocks=[
131+
StoryBlock(prose="Only about the second."),
132+
StoryBlock(view=2),
133+
])])
134+
135+
await report._annotate_report()
136+
137+
assert _kinds(report).count("view") == 2
138+
139+
140+
async def test_report_annotate_only_selected_sections(llm, tiny_source):
141+
from lumen.ai.agents.story import Story, StoryBlock
142+
143+
report = Report(
144+
Section(ChartAction(source=tiny_source, label='Chart A'), title='Section A'),
145+
Section(ChartAction(source=tiny_source, label='Chart B'), title='Section B'),
146+
title='R',
140147
llm=llm,
141148
)
142149
await report.execute()
143150
report[1].include_in_export = False
144-
llm.set_responses([
145-
Story(chain_of_thought="c", title="S", markdown="overall"),
146-
SectionNote(chain_of_thought="c", markdown="Note for A."),
147-
])
151+
llm.set_responses([Story(chain_of_thought="c", title="S", blocks=[
152+
StoryBlock(prose="p"), StoryBlock(view=1),
153+
])])
148154

149155
await report._annotate_report()
150156

151-
nb = _nb_text(report)
152-
assert "Note for A." in nb
153-
assert "Note for B." not in nb
157+
# Only the selected section's chart is catalogued.
158+
assert _kinds(report).count("view") == 1
154159

155160

156-
async def test_report_annotate_is_idempotent(llm):
157-
from lumen.ai.agents.story import SectionNote, Story
161+
async def test_report_annotate_is_idempotent(llm, tiny_source):
162+
from lumen.ai.agents.story import Story, StoryBlock
158163

159-
report = Report(Section(A(order=[]), title='Section A'), title='My Report', llm=llm)
164+
report = Report(
165+
Section(ChartAction(source=tiny_source, label='Chart A'), title='Section A'),
166+
title='R', llm=llm,
167+
)
160168
await report.execute()
161169

162-
llm.set_responses([Story(chain_of_thought="c", title="S1", markdown="v1"), SectionNote(chain_of_thought="c", markdown="n1")])
170+
llm.set_responses([Story(chain_of_thought="c", title="S1", blocks=[
171+
StoryBlock(prose="v1"), StoryBlock(view=1),
172+
])])
163173
await report._annotate_report()
164-
llm.set_responses([Story(chain_of_thought="c", title="S2", markdown="v2"), SectionNote(chain_of_thought="c", markdown="n2")])
174+
llm.set_responses([Story(chain_of_thought="c", title="S2", blocks=[
175+
StoryBlock(prose="v2"), StoryBlock(view=1),
176+
])])
165177
await report._annotate_report()
166178

167179
nb = _nb_text(report)
168-
assert "v2" in nb and "n2" in nb
169-
assert "v1" not in nb and "n1" not in nb
180+
assert "v2" in nb and "v1" not in nb
181+
assert "S2" in nb and "S1" not in nb
170182

171183

172-
async def test_report_annotate_no_llm_is_noop():
173-
report = Report(Section(A(order=[]), title='Section A'), title='My Report')
184+
async def test_report_annotate_no_llm_is_noop(tiny_source):
185+
report = Report(
186+
Section(ChartAction(source=tiny_source, label='Chart A'), title='Section A'),
187+
title='R',
188+
)
174189
await report.execute()
175190

176191
await report._annotate_report() # must not raise
177192

178-
nb = _nb_text(report)
179-
assert "A done" in nb
193+
# Without a story the chart still exports on its own.
194+
assert "lm.Pipeline.from_spec" in _nb_text(report)
195+
196+
197+
async def test_report_story_dialog_presets_and_generate(llm, tiny_source):
198+
from lumen.ai.agents.story import Story, StoryBlock
199+
200+
report = Report(
201+
Section(ChartAction(source=tiny_source, label='Chart A'), title='Section A'),
202+
title='R', llm=llm,
203+
)
204+
await report.execute()
205+
206+
# Clicking Annotate opens the guidance dialog instead of generating immediately.
207+
report._open_story_dialog()
208+
assert report._story_dialog.open is True
209+
210+
# A tone preset fills the guidance box.
211+
report._apply_story_preset(text="Executive summary tone.")
212+
assert report._story_guidance.value == "Executive summary tone."
213+
214+
# Generate runs the annotation with that guidance and closes the dialog.
215+
llm.set_responses([Story(chain_of_thought="c", title="S", blocks=[
216+
StoryBlock(prose="p"), StoryBlock(view=1),
217+
])])
218+
await report._generate_story()
219+
220+
assert report._story_dialog.open is False
221+
assert "prose" in _kinds(report)
222+
223+
224+
async def test_report_view_toggle_switches_story_and_sections(llm, tiny_source):
225+
from lumen.ai.agents.story import Story, StoryBlock
226+
227+
report = Report(
228+
Section(ChartAction(source=tiny_source, label='Chart A'), title='Section A'),
229+
title='R', llm=llm,
230+
)
231+
await report.execute()
232+
llm.set_responses([Story(chain_of_thought="c", title="Headline", blocks=[
233+
StoryBlock(prose="Story prose."), StoryBlock(view=1),
234+
])])
235+
await report._annotate_report()
236+
237+
# After annotate the story is shown and exported, and the toggle is visible.
238+
assert report._show_story is True
239+
assert report._view_toggle.visible is True
240+
assert "Story prose." in _nb_text(report)
241+
242+
# Toggle to the section view: the story is kept, export follows the sections.
243+
report._toggle_view()
244+
assert report._show_story is False
245+
assert report._story_blocks # still remembered
246+
nb_sections = _nb_text(report)
247+
assert "Story prose." not in nb_sections
248+
assert "lm.Pipeline.from_spec" in nb_sections
249+
250+
# Toggle again: the story returns without regenerating.
251+
report._toggle_view()
252+
assert report._show_story is True
253+
assert "Story prose." in _nb_text(report)
254+
255+
256+
async def test_report_reset_discards_story(llm, tiny_source):
257+
from lumen.ai.agents.story import Story, StoryBlock
258+
259+
report = Report(
260+
Section(ChartAction(source=tiny_source, label='Chart A'), title='Section A'),
261+
title='R', llm=llm,
262+
)
263+
await report.execute()
264+
llm.set_responses([Story(chain_of_thought="c", title="H", blocks=[
265+
StoryBlock(prose="p"), StoryBlock(view=1),
266+
])])
267+
await report._annotate_report()
268+
assert report._story_blocks
269+
270+
report.reset()
271+
272+
assert report._story_blocks == []
273+
assert report._show_story is False
274+
assert report._view_toggle.visible is False
180275

181276

182277
async def test_report_outline_defaults_empty():

0 commit comments

Comments
 (0)