Skip to content

Commit 6931856

Browse files
Add issue_ref threading and outcome traces to live FAQ triage
1 parent e2d8ecc commit 6931856

2 files changed

Lines changed: 117 additions & 4 deletions

File tree

faq_automation/cli.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
65140
def 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("\nProcessing 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"\nDecision: {faq_decision.action}")
@@ -142,6 +221,19 @@ def main():
142221
print("\nGenerating 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:

faq_automation/rag_agent.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,18 +320,36 @@ def build_messages(self, question: str, answer: str, num_results: int = 5) -> Li
320320
]
321321

322322
@track
323-
def process_proposal(self, question: str, answer: str, num_results: int = 5) -> FAQDecision:
323+
def process_proposal(
324+
self,
325+
question: str,
326+
answer: str,
327+
num_results: int = 5,
328+
issue_ref: dict | None = None,
329+
) -> FAQDecision:
324330
"""
325331
Process a new FAQ proposal
326332
327333
Args:
328334
question: The proposed question
329335
answer: The proposed answer
330336
num_results: Number of similar FAQs to retrieve (default: 5)
337+
issue_ref: Optional {issue_number, issue_url, course} dict. It lands
338+
in the trace input via @track automatically and, when present,
339+
groups this trace onto thread faq-issue-{n} for issue-level history.
331340
332341
Returns:
333342
FAQDecision object with action and all necessary information
334343
"""
344+
if issue_ref and isinstance(issue_ref, dict):
345+
try:
346+
from opik import opik_context
347+
348+
n = issue_ref.get("issue_number")
349+
if n is not None:
350+
opik_context.update_current_trace(thread_id=f"faq-issue-{n}")
351+
except Exception:
352+
pass
335353
messages = self.build_messages(question, answer, num_results)
336354

337355
# Call OpenAI with structured output
@@ -349,7 +367,8 @@ def process_faq_proposal(
349367
question: str,
350368
answer: str,
351369
openai_api_key: str,
352-
model: str = DEFAULT_MODEL
370+
model: str = DEFAULT_MODEL,
371+
issue_ref: dict | None = None,
353372
) -> FAQDecision:
354373
"""
355374
Convenience function to process a single FAQ proposal
@@ -360,9 +379,11 @@ def process_faq_proposal(
360379
answer: The proposed answer
361380
openai_api_key: OpenAI API key
362381
model: OpenAI model to use (default: DEFAULT_MODEL)
382+
issue_ref: Optional {issue_number, issue_url, course} dict, passed
383+
through to FAQAgent.process_proposal for trace threading.
363384
364385
Returns:
365386
FAQDecision object
366387
"""
367388
agent = FAQAgent(course_dir, openai_api_key, model)
368-
return agent.process_proposal(question, answer)
389+
return agent.process_proposal(question, answer, issue_ref=issue_ref)

0 commit comments

Comments
 (0)