llm evals
Browse files- app.py +16 -0
- requirements.txt +4 -1
- sms_spam/critique.txt +29 -0
- sms_spam/prompt.txt +41 -0
- sms_spam/queries.py +114 -0
- sms_spam/sms.csv +40 -0
- sms_spam/utils.py +247 -0
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 thats the way u feel. Thats 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)
|