kevinhug commited on
Commit
4c3d0df
·
1 Parent(s): e76ce07
app.py CHANGED
@@ -571,4 +571,20 @@ By analyzing behavioral data, preferences, and historical purchases, a recommend
571
  If your product discovery experience isn’t working as hard as your marketing budget, it’s time to make your catalog intelligent—with recommendations that convert.
572
  """)
573
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  demo.launch(allowed_paths=["."])
 
571
  If your product discovery experience isn’t working as hard as your marketing budget, it’s time to make your catalog intelligent—with recommendations that convert.
572
  """)
573
 
574
+ with gr.Tab("LLM Evals"):
575
+ gr.Markdown("""
576
+ 🏦 LLMs for Application Security in Personal Banking
577
+ ====================
578
+ What happens when your generative AI exposes customer data before you even launch?
579
+
580
+ LLM evals reduce security risks in generative AI banking apps by identifying vulnerabilities and guiding secure fixes.
581
+
582
+ Personal banking apps increasingly rely on generative AI—but insecure logic and hallucinations expose sensitive customer data. LLM evals help assess code and AI-generated responses for correctness, task completion, hallucination risk, and safety—enabling proactive guardrails against vulnerabilities before deployment.
583
+
584
+ I’ve led cross-functional model risk initiatives, building pipelines that transform LLM evaluations into automated alerts and remediation workflows—strengthening regulatory compliance and protecting customer trust.
585
+
586
+ Using open-source frameworks, I identify flaws in LLM prompt and translate risks into explainable insights for business, risk, and engineering stakeholders.
587
+ https://postimg.cc/3WtG4ZK2
588
+ """)
589
+
590
  demo.launch(allowed_paths=["."])
requirements.txt CHANGED
@@ -31,4 +31,7 @@ numpy
31
  #KNOWLEDGE GRAPH
32
  graphviz
33
  pydantic
34
- instructor[groq]
 
 
 
 
31
  #KNOWLEDGE GRAPH
32
  graphviz
33
  pydantic
34
+ instructor[groq]
35
+
36
+ #LLM evals
37
+ #inspect_ai
sms_spam/critique.txt ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Background
2
+
3
+ KevinSecure is an observability platform that allows you to predict spam sms.
4
+
5
+ ## Instructions
6
+
7
+ You are an SMS spam evaluator that has advanced capabilities to judge if a SMS spam or not. You understand the nuances of the marketing sms, including what is likely to be most illegal in fraud perspective.
8
+ You are given the following three inputs: (1) SMS, and (2) The answer(ANSWER). Your job is to evaluate and critique the ANSWER relative to the provided SMS
9
+
10
+ The critiques must be provided in the same json format as provided in the few shots examples below:
11
+
12
+ ---
13
+ SMS: WINNER!! As a valued network customer you have been selected to receivea £900 prize reward! To claim call 09061701461. Claim code KL341. Valid 12 hours only.
14
+ ANSWER: spam
15
+
16
+ {"critique": "The response is correct, as it is provide incentive to call back for finding customer", "outcome": "good"}
17
+
18
+ ---
19
+ SMS: I'm gonna be home soon and i don't want to talk about this stuff anymore tonight, k? I've cried enough today.
20
+ ANSWER: spam
21
+
22
+ {"critique": "The response is incorrect, as it is non-marketing message", "outcome": "bad"}
23
+
24
+ ### TODO
25
+ For the below SMS, ANSWER provide a critique as JSON in the format {"critique": "...", "outcome": "good|bad"} as shown above. Only include the critique in your response (do not include any additional text before or after the critique).
26
+
27
+ SMS: {{prompt}}
28
+ ANSWER: {{answer}}
29
+
sms_spam/prompt.txt ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Predict result based on SMS.
2
+
3
+ Here are few shot example:
4
+ SMS: WINNER!! As a valued network customer you have been selected to receivea £900 prize reward! To claim call 09061701461. Claim code KL341. Valid 12 hours only.
5
+ ANSWER: spam
6
+
7
+ SMS: Had your mobile 11 months or more? U R entitled to Update to the latest colour mobiles with camera for Free! Call The Mobile Update Co FREE on 08002986030
8
+ ANSWER: spam
9
+
10
+ SMS: I'm gonna be home soon and i don't want to talk about this stuff anymore tonight, k? I've cried enough today.
11
+ ANSWER: ham
12
+
13
+ SMS: SIX chances to win CASH! From 100 to 20,000 pounds txt> CSH11 and send to 87575. Cost 150p/day, 6days, 16+ TsandCs apply Reply HL 4 info
14
+ ANSWER: spam
15
+
16
+ SMS: URGENT! You have won a 1 week FREE membership in our £100,000 Prize Jackpot! Txt the word: CLAIM to No: 81010 T&C www.dbuk.net LCCLTD POBOX 4403LDNW1A7RW18
17
+ ANSWER: spam
18
+
19
+ SMS: I've been searching for the right words to thank you for this breather. I promise i wont take your help for granted and will fulfil my promise. You have been wonderful and a blessing at all times.
20
+ ANSWER: ham
21
+
22
+ SMS: I HAVE A DATE ON SUNDAY WITH WILL!!
23
+ ANSWER: ham
24
+
25
+ SMS: XXXMobileMovieClub: To use your credit, click the WAP link in the next txt message or click here>> http://wap. xxxmobilemovieclub.com?n=QJKGIGHJJGCBL
26
+ ANSWER: spam
27
+
28
+ SMS: Oh k...i'm watching here:)
29
+ ANSWER: ham
30
+
31
+ SMS: Eh u remember how 2 spell his name... Yes i did. He v naughty make until i v wet.
32
+ ANSWER: ham
33
+
34
+ SMS: Fine if that’s the way u feel. That’s the way its gota b
35
+ ANSWER: ham
36
+
37
+ ---
38
+ Predict whether the SMS is spam or ham in ANSWER, without any comments.
39
+
40
+ SMS: {{prompt}}
41
+ ANSWER:
sms_spam/queries.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ uv add -r requirements.txt
3
+ uv run -- inspect eval queries.py --model ollama/deepseek-r1 --limit 20
4
+ uv run -- inspect view
5
+ """
6
+
7
+ import json
8
+
9
+ from inspect_ai import task, Task
10
+ from inspect_ai.dataset import csv_dataset, FieldSpec
11
+ from inspect_ai.model import get_model
12
+ from inspect_ai.scorer import accuracy, scorer, Score, CORRECT, INCORRECT, match
13
+ from inspect_ai.solver import system_message, generate, solver
14
+ from inspect_ai.util import resource
15
+
16
+ from utils import is_valid, json_completion
17
+ from typing import Literal
18
+
19
+ @task
20
+ def validate():
21
+ return eval_task(scorer=match("any")) #validate_scorer())
22
+
23
+
24
+ @task
25
+ def critique():
26
+ return eval_task(scorer=critique_scorer())
27
+
28
+
29
+ # shared task implementation parmaeterized by scorer
30
+ def eval_task(scorer):
31
+
32
+ # read dataset
33
+ dataset = csv_dataset(
34
+ csv_file="sms.csv",
35
+ sample_fields=FieldSpec(
36
+ input="input",
37
+ target="target"
38
+ ),
39
+ shuffle=True
40
+ )
41
+
42
+ # create eval task
43
+ return Task(
44
+ dataset=dataset,
45
+ plan=[
46
+ system_message("spam detector to determine spam or ham based on SMS."),
47
+ prompt_with_schema(),
48
+ generate()
49
+ ],
50
+ scorer=scorer
51
+ )
52
+
53
+
54
+ @solver
55
+ def prompt_with_schema():
56
+
57
+ prompt_template = resource("prompt.txt")
58
+
59
+ async def solve(state, generate):
60
+ # build the prompt
61
+ state.user_prompt.text = prompt_template.replace(
62
+ "{{prompt}}", state.input #state.user_prompt.text
63
+ )
64
+ return state
65
+
66
+ return solve
67
+
68
+
69
+ @scorer(metrics=[accuracy()])
70
+ def validate_scorer():
71
+
72
+ async def score(state, target):
73
+
74
+ # check for valid query
75
+ query = json_completion(state.output.completion).strip()
76
+ if query==target:
77
+ value=CORRECT
78
+ else:
79
+ value=INCORRECT
80
+
81
+ # return score w/ query that was extracted
82
+ return Score(value=value, answer=query)
83
+
84
+ return score
85
+
86
+
87
+ @scorer(metrics=[accuracy()])
88
+ def critique_scorer(model = "ollama/deepscaler"):
89
+
90
+ async def score(state, target):
91
+
92
+ # build the critic prompt
93
+ query = state.output.completion.strip()
94
+ critic_prompt = resource("critique.txt").replace(
95
+ "{{prompt}}", state.input #state.user_prompt.text
96
+ ).replace(
97
+ "{{answer}}", query
98
+ )
99
+
100
+ # run the critique
101
+ result = await get_model(model).generate(critic_prompt)
102
+ try:
103
+ parsed = json.loads(json_completion(result.completion))
104
+ value = CORRECT if target.text == query else INCORRECT
105
+ explanation = parsed["critique"]
106
+ except (json.JSONDecodeError, KeyError):
107
+ value = INCORRECT
108
+ explanation = f"JSON parsing error:\n{result.completion}"
109
+
110
+ # return value and explanation (critique text)
111
+ return Score(answer=query, value=value, explanation=explanation)
112
+
113
+ return score
114
+
sms_spam/sms.csv ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ target,input
2
+ ham,Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...
3
+ ham,Ok lar... Joking wif u oni...
4
+ spam,Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's
5
+ ham,U dun say so early hor... U c already then say...
6
+ ham,Nah I don't think he goes to usf, he lives around here though
7
+ spam,FreeMsg Hey there darling it's been 3 week's now and no word back! I'd like some fun you up for it still? Tb ok! XxX std chgs to send, £1.50 to rcv
8
+ ham,Even my brother is not like to speak with me. They treat me like aids patent.
9
+ ham,As per your request 'Melle Melle (Oru Minnaminunginte Nurungu Vettam)' has been set as your callertune for all Callers. Press *9 to copy your friends Callertune
10
+ spam,England v Macedonia - dont miss the goals/team news. Txt ur national team to 87077 eg ENGLAND to 87077 Try:WALES, SCOTLAND 4txt/ú1.20 POBOXox36504W45WQ 16+
11
+ ham,Is that seriously how you spell his name?
12
+ ham,I‘m going to try for 2 months ha ha only joking
13
+ ham,So ü pay first lar... Then when is da stock comin...
14
+ ham,Aft i finish my lunch then i go str down lor. Ard 3 smth lor. U finish ur lunch already?
15
+ ham,Ffffffffff. Alright no way I can meet up with you sooner?
16
+ ham,Just forced myself to eat a slice. I'm really not hungry tho. This sucks. Mark is getting worried. He knows I'm sick when I turn down pizza. Lol
17
+ ham,Lol your always so convincing.
18
+ ham,Did you catch the bus ? Are you frying an egg ? Did you make a tea? Are you eating your mom's left over dinner ? Do you feel my Love ?
19
+ ham,I'm back & we're packing the car now, I'll let you know if there's room
20
+ ham,Ahhh. Work. I vaguely remember that! What does it feel like? Lol
21
+ ham,Wait that's still not all that clear, were you not sure about me being sarcastic or that that's why x doesn't want to live with us
22
+ ham,Yeah he got in at 2 and was v apologetic. n had fallen out and she was actin like spoilt child and he got caught up in that. Till 2! But we won't go there! Not doing too badly cheers. You?
23
+ ham,K tell me anything about you.
24
+ ham,For fear of fainting with the of all that housework you just did? Quick have a cuppa
25
+ spam,Thanks for your subscription to Ringtone UK your mobile will be charged £5/month Please confirm by replying YES or NO. If you reply NO you will not be charged
26
+ ham,Yup... Ok i go home look at the timings then i msg ü again... Xuhui going to learn on 2nd may too but her lesson is at 8am
27
+ ham,Oops, I'll let you know when my roommate's done
28
+ ham,I see the letter B on my car
29
+ ham,Anything lor... U decide...
30
+ ham,Hello! How's you and how did saturday go? I was just texting to see if you'd decided to do anything tomo. Not that i'm trying to invite myself or anything!
31
+ ham,Pls go ahead with watts. I just wanted to be sure. Do have a great weekend. Abiola
32
+ ham,Did I forget to tell you ? I want you , I need you, I crave you ... But most of all ... I love you my sweet Arabian steed ... Mmmmmm ... Yummy
33
+ spam,07732584351 - Rodger Burns - MSG = We tried to call you re your reply to our sms for a free nokia mobile + free camcorder. Please call now 08000930705 for delivery tomorrow
34
+ ham,WHO ARE YOU SEEING?
35
+ ham,Great! I hope you like your man well endowed. I am <#> inches...
36
+ ham,No calls..messages..missed calls
37
+ ham,Didn't you get hep b immunisation in nigeria.
38
+ ham,Fair enough, anything going on?
39
+ ham,Yeah hopefully, if tyler can't do it I could maybe ask around a bit
40
+ ham,U don't know how stubborn I am. I didn't even want to go to the hospital. I kept telling Mark I'm not a weak sucker. Hospitals are for weak suckers.
sms_spam/utils.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import json
3
+
4
+
5
+ # sometimes models will enclose the JSON in markdown! (e.g. ```json)
6
+ # this function removes those delimiters should they be there
7
+ def json_completion(completion):
8
+ completion = re.sub(r'^```json\n', '', completion.strip())
9
+ completion = re.sub(r'\n```$', '', completion)
10
+ return completion
11
+
12
+
13
+
14
+ class InvalidQueryException(Exception):
15
+ def __init__(self, message, query=None):
16
+ self.message = message
17
+ self.query = query
18
+ if query:
19
+ self.message += f"\nQuery: {self.query}"
20
+ super().__init__(self.message)
21
+
22
+
23
+ def is_valid(query_spec:str, columns:str, check_runnable=True):
24
+ "Test if a query is valid"
25
+ try:
26
+ check_query(query_spec, columns, check_runnable)
27
+ return True
28
+ except (KeyError, InvalidQueryException):
29
+ return False
30
+
31
+ def check_query(query_spec:str, columns:str, check_runnable=True):
32
+ "Raise an exception if a query is invalid."
33
+ query_spec = query_spec.replace("'", '"')
34
+ try:
35
+ spec = json.loads(query_spec)
36
+ except json.decoder.JSONDecodeError:
37
+ raise InvalidQueryException(f"JSON parsing error:\n{query_spec}", query_spec)
38
+
39
+ valid_calculate_ops = [
40
+ "COUNT",
41
+ "COUNT_DISTINCT",
42
+ "HEATMAP",
43
+ "CONCURRENCY",
44
+ "SUM",
45
+ "AVG",
46
+ "MAX",
47
+ "MIN",
48
+ "P001",
49
+ "P01",
50
+ "P05",
51
+ "P10",
52
+ "P25",
53
+ "P50",
54
+ "P75",
55
+ "P90",
56
+ "P95",
57
+ "P99",
58
+ "P999",
59
+ "RATE_AVG",
60
+ "RATE_SUM",
61
+ "RATE_MAX",
62
+ ]
63
+
64
+ valid_filter_ops = [
65
+ "=",
66
+ "!=",
67
+ ">",
68
+ ">=",
69
+ "<",
70
+ "<=",
71
+ "starts-with",
72
+ "does-not-start-with",
73
+ "exists",
74
+ "does-not-exist",
75
+ "contains",
76
+ "does-not-contain",
77
+ "in",
78
+ "not-in",
79
+ ]
80
+
81
+ if spec == {} or isinstance(spec, float):
82
+ raise InvalidQueryException("Query spec cannot be empty.", query_spec)
83
+
84
+ if isinstance(spec, str):
85
+ raise InvalidQueryException("Query spec was not parsed to json.", query_spec)
86
+
87
+ if "calculations" in spec:
88
+ for calc in spec["calculations"]:
89
+ if "op" not in calc:
90
+ raise InvalidQueryException(f"{calc}: Calculation must have an op.", query_spec)
91
+
92
+ if calc["op"] not in valid_calculate_ops:
93
+ raise InvalidQueryException(f"Invalid calculation: {calc['op']}", query_spec)
94
+
95
+ if calc["op"] == "COUNT" or calc["op"] == "CONCURRENCY":
96
+ if "column" in calc:
97
+ raise InvalidQueryException(f"{calc}: {calc['op']} cannot take a column as input.", query_spec)
98
+ else:
99
+ if "column" not in calc:
100
+ raise InvalidQueryException(f"{calc}: {calc['op']} must take a column as input.", query_spec)
101
+
102
+ if check_runnable and calc["column"] not in columns:
103
+ raise InvalidQueryException(f"Invalid column: {calc['column']}", query_spec)
104
+
105
+
106
+ if "filters" in spec:
107
+ for filter in spec["filters"]:
108
+ if not isinstance(filter, dict):
109
+ raise InvalidQueryException("filter of type other than dict found in query.", query_spec)
110
+ if "op" not in filter:
111
+ raise InvalidQueryException("No op found in filter.", query_spec)
112
+ if filter["op"] not in valid_filter_ops:
113
+ raise InvalidQueryException(f"Invalid filter: {filter['op']}", query_spec)
114
+
115
+
116
+ if check_runnable and filter["column"] not in columns:
117
+ raise InvalidQueryException(f"Invalid column: {filter['column']}", query_spec)
118
+
119
+
120
+ if filter["op"] == "exists" or filter["op"] == "does-not-exist":
121
+ if "value" in filter:
122
+ raise InvalidQueryException(f"{filter}: {filter['op']} cannot take a value as input.", query_spec)
123
+
124
+ else:
125
+ if filter["op"] == "in" or filter["op"] == "not-in":
126
+ if not isinstance(filter["value"], list):
127
+ raise InvalidQueryException(f"{filter}: {filter['op']} must take a list as input.", query_spec)
128
+
129
+ else:
130
+ if "value" not in filter:
131
+ raise InvalidQueryException(f"{filter}: {filter['op']} must take a value as input.", query_spec)
132
+
133
+ if "filter_combination" in spec:
134
+ if isinstance(spec["filter_combination"], str) and spec[
135
+ "filter_combination"
136
+ ].lower() not in ["and", "or"]:
137
+ raise InvalidQueryException(f"Invalid filter combination: {spec['filter_combination']}", query_spec)
138
+
139
+
140
+ if "breakdowns" in spec:
141
+ for breakdown in spec["breakdowns"]:
142
+ if check_runnable and breakdown not in columns:
143
+ raise InvalidQueryException(f"Invalid column: {breakdown}", query_spec)
144
+
145
+
146
+ if "orders" in spec:
147
+ for order in spec["orders"]:
148
+ if "order" not in order:
149
+ raise InvalidQueryException(f"Invalid order without orders key: {query_spec}")
150
+ if order["order"] != "ascending" and order["order"] != "descending":
151
+ raise InvalidQueryException(f"Invalid order: {order['order']}", query_spec)
152
+
153
+ if "op" in order:
154
+ if order["op"] not in valid_calculate_ops:
155
+ raise InvalidQueryException(f"Invalid order: {order['op']}", query_spec)
156
+
157
+
158
+ if not any(calc["op"] == order["op"] for calc in spec.get("calculations", [])):
159
+ raise InvalidQueryException(f"{order}: Order op must be present in calculations: {order['op']}", query_spec)
160
+
161
+ if order["op"] == "COUNT" or order["op"] == "CONCURRENCY":
162
+ if "column" in order:
163
+ raise InvalidQueryException(f"{order}: {order['op']} cannot take a column as input.", query_spec)
164
+
165
+ else:
166
+ if "column" not in order:
167
+ raise InvalidQueryException(f"{order}: {order['op']} must take a column as input.", query_spec)
168
+
169
+ if check_runnable and order["column"] not in columns:
170
+ raise InvalidQueryException(f"{order}: Invalid column in order: {order['column']}", query_spec)
171
+
172
+ else:
173
+ if "column" not in order:
174
+ raise InvalidQueryException(f"{order}: Order must take a column or op as input.", query_spec)
175
+
176
+ if check_runnable and order["column"] not in columns:
177
+ raise InvalidQueryException(f"{order}: Invalid column in order: {order['column']}", query_spec)
178
+
179
+
180
+ if "havings" in spec:
181
+ for having in spec["havings"]:
182
+ if "calculate_op" not in having:
183
+ raise InvalidQueryException(f"{having}: Having must have a calculate_op.", query_spec)
184
+
185
+ if "value" not in having:
186
+ raise InvalidQueryException(f"{having}: Having must have a value.", query_spec)
187
+
188
+ if "op" not in having:
189
+ raise InvalidQueryException(f"{having}: Having must have an op.", query_spec)
190
+
191
+ if having["calculate_op"] == "HEATMAP":
192
+ raise InvalidQueryException("HEATMAP is not supported in having.", query_spec)
193
+
194
+ if (
195
+ having["calculate_op"] == "COUNT"
196
+ or having["calculate_op"] == "CONCURRENCY"
197
+ ):
198
+ if "column" in having:
199
+ raise InvalidQueryException(f"{having}: {having['calculate_op']} cannot take a column as input.", query_spec)
200
+
201
+ else:
202
+ if "column" not in having:
203
+ raise InvalidQueryException(f"{having}: {having['calculate_op']} must take a column as input.", query_spec)
204
+
205
+ if check_runnable and having["column"] not in columns:
206
+ raise InvalidQueryException(f"{having}: Invalid column in having: {having['column']}", query_spec)
207
+
208
+
209
+ if "time_range" in spec:
210
+ if "start_time" in spec and "end_time" in spec:
211
+ raise InvalidQueryException("Time range cannot be specified with start_time and end_time.", query_spec)
212
+
213
+ if not isinstance(spec["time_range"], int):
214
+ raise InvalidQueryException(f"time_range must be an int: {spec['time_range']}", query_spec)
215
+
216
+
217
+ if "start_time" in spec:
218
+ if not isinstance(spec["start_time"], int):
219
+ raise InvalidQueryException(f"start_time must be an int: {spec['start_time']}", query_spec)
220
+
221
+
222
+ if "end_time" in spec:
223
+ if not isinstance(spec["end_time"], int):
224
+ raise InvalidQueryException(f"end_time must be an int: {spec['end_time']}", query_spec)
225
+
226
+
227
+ if "granularity" in spec:
228
+ if not isinstance(spec["granularity"], int):
229
+ raise InvalidQueryException(f"granularity must be an int: {spec['granularity']}", query_spec)
230
+
231
+
232
+ time_range = (
233
+ spec["time_range"]
234
+ if "time_range" in spec
235
+ else spec["end_time"] - spec["start_time"]
236
+ if "start_time" in spec and "end_time" in spec
237
+ else 7200
238
+ )
239
+ if spec["granularity"] > time_range / 10:
240
+ raise InvalidQueryException(f"granularity must be <= time_range / 10: {spec['granularity']}", query_spec)
241
+
242
+ if spec["granularity"] < time_range / 1000:
243
+ raise InvalidQueryException(f"granularity must be >= time_range / 1000: {spec['granularity']}", query_spec)
244
+
245
+ if "limit" in spec:
246
+ if not isinstance(spec["limit"], int):
247
+ raise InvalidQueryException(f"limit must be an int: {spec['limit']}", query_spec)