11import os
22
3+ from langchain_core .output_parsers import PydanticOutputParser
34from pydantic import ValidationError
45
56from osa_tool .config .settings import ConfigManager
67from osa_tool .core .git .metadata import RepositoryMetadata
78from osa_tool .core .llm .llm import ModelHandler , ModelHandlerFactory
9+ from osa_tool .core .models .event import OperationEvent
810from osa_tool .operations .analysis .repository_report .response_validation import (
911 RepositoryReport ,
1012 RepositoryStructure ,
1315 OverallAssessment ,
1416 AfterReportBlock ,
1517 AfterReport ,
18+ AfterReportSummary ,
19+ AfterReportBlocksPlan ,
1620)
1721from osa_tool .tools .repository_analysis .sourcerank import SourceRank
1822from osa_tool .utils .logger import logger
@@ -90,11 +94,17 @@ def _extract_presence_files(self) -> list[str]:
9094
9195
9296class AfterReportTextGenerator :
93- def __init__ (self , config_manger : ConfigManager , what_has_been_done : list [tuple [str , bool ]]) -> None :
97+ def __init__ (
98+ self ,
99+ config_manger : ConfigManager ,
100+ completed_tasks : list [tuple [str , bool ]],
101+ task_results : dict [str , dict ] | None = None ,
102+ ) -> None :
94103 self .config_manager = config_manger
95104 self .model_settings = self .config_manager .get_model_settings ("general" )
96- self .prompts = self .config_manager .config .prompts
97- self .what_has_been_done = what_has_been_done
105+ self .prompts = self .config_manager .get_prompts ()
106+ self .completed_tasks = completed_tasks
107+ self .task_results = task_results or {}
98108 self .model_handler : ModelHandler = ModelHandlerFactory .build (self .model_settings )
99109
100110 def make_request (self ) -> AfterReport :
@@ -104,32 +114,90 @@ def make_request(self) -> AfterReport:
104114 Returns:
105115 The generated OSA work summary response from the model.
106116 """
107- formatted_tasks = "\n " .join (
108- f"Task { i } . { n } : { 'Yes' if d else 'No' } " for i , (n , d ) in enumerate (self .what_has_been_done )
109- )
110- json_prompt = PromptBuilder .render (
111- self .prompts .get ("analysis.after_report_blocks_prompt" ),
112- tasks_list = formatted_tasks ,
113- )
114- summary_prompt = PromptBuilder .render (
115- self .prompts .get ("analysis.after_report_text_prompt" ),
116- tasks_list = formatted_tasks ,
117- )
117+ performed_lookup = {name : done for name , done in self .completed_tasks }
118+ operations_text = self ._operations_to_text (self .completed_tasks , self .task_results )
118119
119120 try :
120- summary = self .model_handler .send_request (prompt = summary_prompt )
121- json_result = self .model_handler .send_and_parse (
122- prompt = json_prompt ,
123- parser = lambda raw : [
124- AfterReportBlock (
125- name = d ["name" ],
126- description = d ["description" ],
127- tasks = [self .what_has_been_done [i ] for i in d ["tasks" ]],
128- )
129- for d in JsonProcessor .parse (raw , expected_type = list )
130- ],
121+ # Summary (structured JSON -> AfterReportSummary)
122+ summary_parser = PydanticOutputParser (pydantic_object = AfterReportSummary )
123+ summary_system = self .prompts .get ("system_messages.after_report_summary" )
124+ summary_prompt = PromptBuilder .render (
125+ self .prompts .get ("analysis.after_report_summary_from_events_prompt" ),
126+ operations = operations_text ,
127+ )
128+ summary_obj : AfterReportSummary = self .model_handler .run_chain (
129+ prompt = summary_prompt ,
130+ parser = summary_parser ,
131+ system_message = summary_system ,
132+ )
133+
134+ # Blocks (structured JSON -> AfterReportBlocksPlan)
135+ blocks_parser = PydanticOutputParser (pydantic_object = AfterReportBlocksPlan )
136+ blocks_system = self .prompts .get ("system_messages.after_report_blocks" )
137+ blocks_prompt = PromptBuilder .render (
138+ self .prompts .get ("analysis.after_report_blocks_from_events_prompt" ),
139+ operations = operations_text ,
140+ )
141+ blocks_plan : AfterReportBlocksPlan = self .model_handler .run_chain (
142+ prompt = blocks_prompt ,
143+ parser = blocks_parser ,
144+ system_message = blocks_system ,
131145 )
132- return AfterReport (summary = summary , blocks = json_result )
146+
147+ blocks : list [AfterReportBlock ] = []
148+ for block in blocks_plan .root :
149+ tasks = [(t , bool (performed_lookup .get (t , False ))) for t in block .tasks ]
150+ blocks .append (AfterReportBlock (name = block .name , description = block .description , tasks = tasks ))
151+
152+ return AfterReport (summary = summary_obj .summary , blocks = blocks )
153+
133154 except Exception as e :
134- logger .error (f"Unexpected error while parsing RepositoryReport: { e } " )
135- raise ValueError (f"Failed to process model response: { e } " )
155+ logger .error ("Unexpected error while generating AfterReport: %s" , e )
156+ raise ValueError (f"Failed to process model response: { e } " ) from e
157+
158+ @staticmethod
159+ def _events_to_text (events : list [OperationEvent ]) -> str :
160+ lines : list [str ] = []
161+ for e in events :
162+ kind = getattr (e .kind , "value" , str (e .kind ))
163+ line = "- %s: %s" % (kind , e .target )
164+ data = getattr (e , "data" , None ) or {}
165+ if data :
166+ line += " (%s)" % ", " .join ("%s=%s" % (k , v ) for k , v in data .items ())
167+ lines .append (line )
168+ return "\n " .join (lines )
169+
170+ def _operations_to_text (self , completed_tasks : list [tuple [str , bool ]], task_results : dict [str , dict ]) -> str :
171+ parts : list [str ] = []
172+
173+ for name , done in completed_tasks :
174+ details = task_results .get (name ) or {}
175+ result = details .get ("result" )
176+ events = details .get ("events" ) or []
177+
178+ # Result (truncate)
179+ result_text = "None"
180+ if result is not None :
181+ result_text = str (result )
182+ if len (result_text ) > 600 :
183+ result_text = result_text [:600 ] + "..."
184+
185+ # Events text
186+ try :
187+ events_text = self ._events_to_text (events )
188+ except Exception :
189+ events_text = "\n " .join ("- %s" % str (e ) for e in events ) if events else ""
190+
191+ parts .append (
192+ "\n " .join (
193+ [
194+ f"Operation: { name } " ,
195+ f"Performed: { 'Yes' if done else 'No' } " ,
196+ f"Result: { result_text } " ,
197+ "Events:" ,
198+ events_text or "- (none)" ,
199+ ]
200+ )
201+ )
202+
203+ return "\n \n ---\n \n " .join (parts )
0 commit comments