33import param
44import yaml
55
6- from panel .pane import Markdown
7- from panel_material_ui import ChatMessage , Typography
86from pydantic import BaseModel , Field
97
108from ...pipeline import Pipeline
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
4655class 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
8682async 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 )
0 commit comments