Skip to content

Commit e7bc294

Browse files
CLDR-18745 cldr_prompt_to_llm.py (#4988)
1 parent 4768b75 commit e7bc294

File tree

1 file changed

+150
-15
lines changed

1 file changed

+150
-15
lines changed
Lines changed: 150 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,168 @@
11
#!/usr/bin/env python3
22
"""
33
Posts the prepared prompt to OpenAI and prints the model's response.
4+
Optionally: posts the JSON back to the JIRA ticket as a comment and updates fields.
5+
46
Usage:
57
python cldr_prompt_to_llm.py CLDR-1234
68
python cldr_prompt_to_llm.py CLDR-1234 --category "Software Bug"
79
python cldr_prompt_to_llm.py CLDR-1234 --auto-category --model gpt-4o-mini
10+
# post back to JIRA:
11+
python cldr_prompt_to_llm.py CLDR-1234 --post-comment
12+
python cldr_prompt_to_llm.py CLDR-1234 --post-comment --update-fields
813
"""
9-
import sys, argparse, json
14+
15+
import sys
16+
import json
17+
import argparse
1018
from openai import OpenAI
19+
from jira import JIRA
20+
1121
from cldr_dynamic_prompter import make_prompt
1222

13-
# ---- Paste your OpenAI creds (no envs required) ----
14-
OPENAI_API_KEY = "API key of openAi"
23+
# ---------------- OpenAI (fill before running) ----------------
24+
OPENAI_API_KEY = "YOUR KEY!!!" # <- your real OpenAI key (don't commit!)
1525
DEFAULT_MODEL = "gpt-4o-mini"
16-
# ----------------------------------------------------
26+
# --------------------------------------------------------------
27+
28+
# ---------------- JIRA (only needed for --post-comment / --update-fields) ---------------
29+
JIRA_SERVER = "https://unicode-org.atlassian.net"
30+
JIRA_USER_EMAIL = "YOUR MAIL ID" # <- your JIRA email
31+
JIRA_API_TOKEN = "YOUR KEY !!!" # <- your JIRA API token (don't commit!)
32+
# ---------------------------------------------------------------------------------------
1733

1834
def _extract_json(text: str) -> str:
19-
"""If the model adds extra text, try to print the first JSON object cleanly."""
35+
"""
36+
If the model returns extra prose, try to extract the first JSON object.
37+
Returns the original text if no JSON can be isolated.
38+
"""
2039
text = text.strip()
2140
try:
22-
json.loads(text); return text
41+
json.loads(text)
42+
return text
2343
except Exception:
2444
pass
45+
2546
s, e = text.find("{"), text.rfind("}")
2647
if s != -1 and e != -1 and e > s:
2748
chunk = text[s:e+1]
2849
try:
29-
json.loads(chunk); return chunk
50+
json.loads(chunk)
51+
return chunk
3052
except Exception:
3153
return chunk
3254
return text
3355

56+
# -------------------- JIRA helpers --------------------
57+
58+
def _jira_client() -> JIRA:
59+
return JIRA(server=JIRA_SERVER, basic_auth=(JIRA_USER_EMAIL, JIRA_API_TOKEN))
60+
61+
def post_result_comment(ticket_key: str, json_text: str) -> None:
62+
"""
63+
Add a comment with the Phase-1/Phase-2 JSON. Uses Jira's {code:json} block.
64+
Adjust visibility if you need it to be restricted.
65+
"""
66+
jira = _jira_client()
67+
body = (
68+
"Classification result (LLM)\n"
69+
"{code:json}\n" + json_text + "\n{code}\n"
70+
"_Posted by CLDR validator tool_"
71+
)
72+
# Example for restricted comment (role):
73+
# jira.add_comment(ticket_key, body, visibility={"type": "role", "value": "Administrators"})
74+
jira.add_comment(ticket_key, body)
75+
76+
def _is_false_like(v) -> bool:
77+
# Helper: handle "false" (str), False (bool), 0, etc.
78+
if isinstance(v, str):
79+
return v.strip().lower() in {"false", "no", "0"}
80+
return not bool(v)
81+
82+
def _is_true_like(v) -> bool:
83+
if isinstance(v, str):
84+
return v.strip().lower() in {"true", "yes", "1"}
85+
return bool(v)
86+
87+
def update_fields_from_result(ticket_key: str, result: dict) -> None:
88+
"""
89+
Example field updates:
90+
- Merge useful labels (spam, out-of-scope, phase1-classified, needs-lang, needs-tc)
91+
- Optionally merge components if JSON includes `components.additions` (Phase 2 style)
92+
"""
93+
jira = _jira_client()
94+
issue = jira.issue(ticket_key)
95+
96+
# ----- labels -----
97+
existing_labels = set(issue.fields.labels or [])
98+
to_add = {"phase1-classified"}
99+
100+
# Phase-1 schemas vary across prompts; check common shapes
101+
spam = result.get("spamCheck", {}).get("isSpam")
102+
scope = result.get("scopeCheck", {}).get("inScope")
103+
needs_lang = (
104+
result.get("needsLanguageSpecialist", {}).get("status") or
105+
result.get("needsLanguageSpecialist")
106+
)
107+
needs_tc = (
108+
result.get("needsEngineeringTC") or
109+
result.get("needsEngineeringWork", {}).get("status")
110+
)
111+
112+
if _is_true_like(spam):
113+
to_add.add("spam")
114+
115+
# out-of-scope can be boolean or "false"/"true"/"unknown"
116+
if isinstance(scope, str):
117+
if scope.strip().lower() == "false":
118+
to_add.add("out-of-scope")
119+
elif scope is not None:
120+
if _is_false_like(scope):
121+
to_add.add("out-of-scope")
122+
123+
if _is_true_like(needs_lang):
124+
to_add.add("needs-language-specialist")
125+
126+
if _is_true_like(needs_tc):
127+
to_add.add("needs-engineering-tc")
128+
129+
new_labels = sorted(existing_labels | to_add)
130+
if new_labels != list(existing_labels):
131+
issue.update(fields={"labels": new_labels})
132+
133+
# ----- components (Phase-2 style JSON) -----
134+
# If your JSON includes something like:
135+
# "components": { "present": [...], "additions": ["Units","Plural Rules"] }
136+
comp_suggestions = (result.get("components") or {}).get("additions", [])
137+
if comp_suggestions:
138+
current_names = {c.name for c in (issue.fields.components or [])}
139+
merged = sorted(current_names | set(comp_suggestions))
140+
if merged != list(current_names):
141+
issue.update(fields={"components": [{"name": n} for n in merged]})
142+
143+
# -------------------- main --------------------
144+
34145
def main():
35-
ap = argparse.ArgumentParser(description="Send CLDR prompt to OpenAI and print the response.")
146+
ap = argparse.ArgumentParser(
147+
description="Send CLDR prompt to OpenAI and print the response (optional: post to JIRA)."
148+
)
36149
ap.add_argument("ticket_key", help="e.g., CLDR-18761")
37-
ap.add_argument("--category", choices=["Data Accuracy","Documentation Issue","Software Bug","Feature Request","Triage"])
38-
ap.add_argument("--auto-category", action="store_true")
39-
ap.add_argument("--model", default=DEFAULT_MODEL)
150+
ap.add_argument(
151+
"--category",
152+
choices=["Data Accuracy", "Documentation Issue", "Software Bug", "Feature Request", "Triage"],
153+
help="Force category; otherwise use default behavior of the prompt generator."
154+
)
155+
ap.add_argument("--auto-category", action="store_true", help="Let the prompter LLM choose a category.")
156+
ap.add_argument("--model", default=DEFAULT_MODEL, help="OpenAI model (default: gpt-4o-mini).")
157+
ap.add_argument("--post-comment", action="store_true", help="Post the JSON result into the JIRA ticket.")
158+
ap.add_argument("--update-fields", action="store_true", help="Update labels/components based on the JSON.")
159+
40160
args = ap.parse_args()
41161

42-
# 1) Prepare prompt (re-uses the function in cldr_dynamic_prompter.py)
162+
# 1) Build the prompt (uses your generator)
43163
prompt = make_prompt(args.ticket_key, category=args.category, auto_category=args.auto_category)
44164

45-
# 2) Post to OpenAI
165+
# 2) Call OpenAI
46166
try:
47167
client = OpenAI(api_key=OPENAI_API_KEY)
48168
resp = client.chat.completions.create(
@@ -55,8 +175,23 @@ def main():
55175
print(f"OpenAI call failed: {e}", file=sys.stderr)
56176
sys.exit(2)
57177

58-
# 3) Print (JSON if present)
59-
print(_extract_json(content))
178+
# 3) Print the (extracted) JSON to stdout
179+
json_text = _extract_json(content)
180+
print(json_text)
181+
182+
# 4) Optionally push back to JIRA
183+
if args.post_comment:
184+
try:
185+
post_result_comment(args.ticket_key, json_text)
186+
except Exception as e:
187+
print(f"Failed to post JIRA comment: {e}", file=sys.stderr)
188+
189+
if args.update_fields:
190+
try:
191+
parsed = json.loads(json_text)
192+
update_fields_from_result(args.ticket_key, parsed)
193+
except Exception as e:
194+
print(f"Skipping field updates (invalid JSON or JIRA error): {e}", file=sys.stderr)
60195

61196
if __name__ == "__main__":
62197
main()

0 commit comments

Comments
 (0)