@@ -62,6 +62,81 @@ def parse_full_issue_body(issue_body: str) -> tuple[str, str, str]:
6262 return course , question , answer
6363
6464
65+ def build_issue_ref (issue_number : int , course : str | None = None ) -> dict :
66+ """
67+ Build the {issue_number, issue_url, course} dict for trace threading.
68+
69+ Repo comes from GITHUB_REPOSITORY (owner/repo), defaulting to
70+ DataTalksClub/faq for local runs.
71+ """
72+ repo = os .environ .get ("GITHUB_REPOSITORY" , "DataTalksClub/faq" )
73+ return {
74+ "issue_number" : issue_number ,
75+ "issue_url" : f"https://github.com/{ repo } /issues/{ issue_number } " ,
76+ "course" : course ,
77+ }
78+
79+
80+ def log_outcome_trace (
81+ issue_ref : dict | None ,
82+ action : str ,
83+ file_path : str | None = None ,
84+ pr_body : str | None = None ,
85+ comment : str | None = None ,
86+ pr_url : str | None = None ,
87+ course : str | None = None ,
88+ ):
89+ """
90+ Best-effort second trace on the same thread as triage.
91+
92+ Logs file_path/pr_body (NEW/UPDATE) or comment (DUPLICATE/WRONG_COURSE),
93+ plus pr_url when known (workflow PR step fills it in later via this same
94+ helper). Disabled gracefully without OPIK_API_KEY; never raises.
95+ """
96+ try :
97+ if not issue_ref or not isinstance (issue_ref , dict ):
98+ return None
99+ n = issue_ref .get ("issue_number" )
100+ if n is None :
101+ return None
102+ if not os .environ .get ("OPIK_API_KEY" ):
103+ return None
104+ from opik import Opik
105+
106+ project = os .environ .get ("OPIK_PROJECT_NAME" , "faq-automation-ci" )
107+ client = Opik (project_name = project )
108+ thread_id = f"faq-issue-{ n } "
109+ resolved_course = course or issue_ref .get ("course" )
110+ client .trace (
111+ name = "faq-outcome" ,
112+ thread_id = thread_id ,
113+ input = {
114+ "issue_number" : n ,
115+ "issue_url" : issue_ref .get ("issue_url" ),
116+ "course" : resolved_course ,
117+ "action" : action ,
118+ },
119+ output = {
120+ "action" : action ,
121+ "file_path" : str (file_path ) if file_path else None ,
122+ "pr_body" : pr_body ,
123+ "comment" : comment ,
124+ "pr_url" : pr_url ,
125+ },
126+ tags = [resolved_course ] if resolved_course else None ,
127+ metadata = {"source" : "faq-automation-cli" },
128+ )
129+ try :
130+ import opik
131+
132+ opik .flush_tracker ()
133+ except Exception :
134+ pass
135+ return thread_id
136+ except Exception :
137+ return None
138+
139+
65140def main ():
66141 """Main CLI entry point"""
67142 parser = argparse .ArgumentParser (description = 'Process FAQ proposal from GitHub issue' )
@@ -93,14 +168,18 @@ def main():
93168 print (f"Error: Course directory { course_dir } does not exist" , file = sys .stderr )
94169 sys .exit (1 )
95170
171+ # Issue ref lands in the triage trace input via @track and threads it.
172+ issue_ref = build_issue_ref (args .issue_number , course )
173+
96174 # Process proposal
97175 print ("\n Processing FAQ proposal with LLM..." )
98176 faq_decision = process_faq_proposal (
99177 course_dir = course_dir ,
100178 question = question ,
101179 answer = answer ,
102180 openai_api_key = openai_api_key ,
103- model = args .model
181+ model = args .model ,
182+ issue_ref = issue_ref ,
104183 )
105184
106185 print (f"\n Decision: { faq_decision .action } " )
@@ -142,6 +221,19 @@ def main():
142221 print ("\n Generating wrong course comment..." )
143222 output ['comment' ] = generate_wrong_course_comment (faq_decision , course )
144223
224+ # Second trace on the same thread with file/comment outcome (pr_url is
225+ # None here — the workflow PR step creates the PR after the CLI exits;
226+ # it can call log_outcome_trace again with the real pr_url).
227+ log_outcome_trace (
228+ issue_ref = issue_ref ,
229+ action = faq_decision .action ,
230+ file_path = output .get ('file_path' ),
231+ pr_body = output .get ('pr_body' ),
232+ comment = output .get ('comment' ),
233+ pr_url = None ,
234+ course = course ,
235+ )
236+
145237 # Write output as JSON for GitHub Actions
146238 output_file = Path (args .output_dir ) / 'faq_decision.json'
147239 with open (output_file , 'w' ) as f :
0 commit comments