1
1
#!/usr/bin/env python3
2
2
"""
3
3
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
+
4
6
Usage:
5
7
python cldr_prompt_to_llm.py CLDR-1234
6
8
python cldr_prompt_to_llm.py CLDR-1234 --category "Software Bug"
7
9
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
8
13
"""
9
- import sys , argparse , json
14
+
15
+ import sys
16
+ import json
17
+ import argparse
10
18
from openai import OpenAI
19
+ from jira import JIRA
20
+
11
21
from cldr_dynamic_prompter import make_prompt
12
22
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!)
15
25
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
+ # ---------------------------------------------------------------------------------------
17
33
18
34
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
+ """
20
39
text = text .strip ()
21
40
try :
22
- json .loads (text ); return text
41
+ json .loads (text )
42
+ return text
23
43
except Exception :
24
44
pass
45
+
25
46
s , e = text .find ("{" ), text .rfind ("}" )
26
47
if s != - 1 and e != - 1 and e > s :
27
48
chunk = text [s :e + 1 ]
28
49
try :
29
- json .loads (chunk ); return chunk
50
+ json .loads (chunk )
51
+ return chunk
30
52
except Exception :
31
53
return chunk
32
54
return text
33
55
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
+
34
145
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
+ )
36
149
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
+
40
160
args = ap .parse_args ()
41
161
42
- # 1) Prepare prompt (re- uses the function in cldr_dynamic_prompter.py )
162
+ # 1) Build the prompt (uses your generator )
43
163
prompt = make_prompt (args .ticket_key , category = args .category , auto_category = args .auto_category )
44
164
45
- # 2) Post to OpenAI
165
+ # 2) Call OpenAI
46
166
try :
47
167
client = OpenAI (api_key = OPENAI_API_KEY )
48
168
resp = client .chat .completions .create (
@@ -55,8 +175,23 @@ def main():
55
175
print (f"OpenAI call failed: { e } " , file = sys .stderr )
56
176
sys .exit (2 )
57
177
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 )
60
195
61
196
if __name__ == "__main__" :
62
197
main ()
0 commit comments