Spaces:
Running
on
L4
Running
on
L4
Commit
·
291adda
1
Parent(s):
dc87cf7
Move PULS code
Browse files- neus_v/puls/llm.py +58 -0
- neus_v/puls/main.py +28 -0
- neus_v/puls/prompts.py +85 -0
- neus_v/puls/puls.py +80 -0
neus_v/puls/llm.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import datetime
|
2 |
+
import json
|
3 |
+
import os
|
4 |
+
|
5 |
+
|
6 |
+
class LLM:
|
7 |
+
def __init__(self, client, save_dir="outputs"):
|
8 |
+
self.client = client
|
9 |
+
self.history = []
|
10 |
+
self.save_dir = save_dir
|
11 |
+
if save_dir:
|
12 |
+
os.makedirs(save_dir, exist_ok=True)
|
13 |
+
|
14 |
+
def prompt(self, p):
|
15 |
+
# Add user message to history
|
16 |
+
user_message = {"role": "user", "content": [{"type": "text", "text": p}]}
|
17 |
+
self.history.append(user_message)
|
18 |
+
|
19 |
+
# Create messages list with history
|
20 |
+
messages = self.history.copy()
|
21 |
+
|
22 |
+
response = self.client.chat.completions.create(
|
23 |
+
model="o1-mini-2024-09-12",
|
24 |
+
messages=self.history,
|
25 |
+
store=False,
|
26 |
+
)
|
27 |
+
|
28 |
+
# Extract assistant response
|
29 |
+
assistant_response = response.choices[0].message.content
|
30 |
+
|
31 |
+
# Add assistant response to history
|
32 |
+
assistant_message = {"role": "assistant", "content": [{"type": "text", "text": assistant_response}]}
|
33 |
+
self.history.append(assistant_message)
|
34 |
+
|
35 |
+
return assistant_response
|
36 |
+
|
37 |
+
def save_history(self, filename="conversation_history.json"):
|
38 |
+
"""Save conversation history to a JSON file"""
|
39 |
+
if not self.save_dir:
|
40 |
+
return
|
41 |
+
|
42 |
+
# Add timestamp to filename
|
43 |
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
44 |
+
base_name, extension = os.path.splitext(filename)
|
45 |
+
timestamped_filename = f"{base_name}_{timestamp}{extension}"
|
46 |
+
|
47 |
+
save_path = os.path.join(self.save_dir, timestamped_filename)
|
48 |
+
try:
|
49 |
+
with open(save_path, "w", encoding="utf-8") as f:
|
50 |
+
json.dump(self.history, f, indent=4, ensure_ascii=False)
|
51 |
+
print(f"Conversation history saved to: {save_path}")
|
52 |
+
except Exception as e:
|
53 |
+
print(f"Failed to save conversation history: {e}")
|
54 |
+
|
55 |
+
def __del__(self):
|
56 |
+
"""Destructor: Saves conversation history when the object is destroyed"""
|
57 |
+
if self.save_dir:
|
58 |
+
self.save_history()
|
neus_v/puls/main.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from puls import *
|
2 |
+
|
3 |
+
import argparse
|
4 |
+
import json
|
5 |
+
import time
|
6 |
+
|
7 |
+
def main():
|
8 |
+
prompt = "a dog sits on a mat while a ball rolls past the dog towards a couch, then the dog picks up the ball and places it beside the couch"
|
9 |
+
modes = [Mode.OBJECT_ACTION_ALIGNMENT, Mode.OVERALL_CONSISTENCY, Mode.OBJECT_EXISTENCE, Mode.SPATIAL_RELATIONSHIP]
|
10 |
+
|
11 |
+
parser = argparse.ArgumentParser(description='Set OpenAI API Key.')
|
12 |
+
parser.add_argument('--openai_key', type=str, help='Your OpenAI API key')
|
13 |
+
args = parser.parse_args()
|
14 |
+
|
15 |
+
start_time = time.time()
|
16 |
+
if args.openai_key:
|
17 |
+
data = PULS(prompt, modes, args.openai_key)
|
18 |
+
else:
|
19 |
+
data = PULS(prompt, modes)
|
20 |
+
end_time = time.time()
|
21 |
+
|
22 |
+
print(prompt)
|
23 |
+
print(json.dumps(data, indent=2))
|
24 |
+
print(f"Elapsed Time: {end_time - start_time}")
|
25 |
+
|
26 |
+
if __name__ == "__main__":
|
27 |
+
main()
|
28 |
+
|
neus_v/puls/prompts.py
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from enum import Enum
|
2 |
+
|
3 |
+
class Mode(Enum):
|
4 |
+
OBJECT_ACTION_ALIGNMENT = 1
|
5 |
+
OBJECT_EXISTENCE = 2
|
6 |
+
OVERALL_CONSISTENCY = 3
|
7 |
+
SPATIAL_RELATIONSHIP = 4
|
8 |
+
|
9 |
+
mode_prompts = {
|
10 |
+
Mode.OBJECT_ACTION_ALIGNMENT: (
|
11 |
+
"\"object_action_alignment\":\n"
|
12 |
+
"Extract actions and their participating objects. Each proposition must describe an action and its related objects.\n"
|
13 |
+
"Example:\n"
|
14 |
+
"\"object_action_alignment\": [\"person holds hotdog\", \"person walks\"]"
|
15 |
+
),
|
16 |
+
Mode.OBJECT_EXISTENCE: (
|
17 |
+
"\"object_existence\":\n"
|
18 |
+
"Extract only the tangible objects mentioned in the prompt.\n"
|
19 |
+
"Example:\n"
|
20 |
+
"\"object_existence\": [\"person\", \"hotdog\", \"car\", \"truck\"]"
|
21 |
+
),
|
22 |
+
Mode.OVERALL_CONSISTENCY: (
|
23 |
+
"\"overall_consistency\":\n"
|
24 |
+
"Extract all meaningful event propositions that describe the combined semantics of objects, actions, and spatial relationships — "
|
25 |
+
"but avoid TL keywords such as 'and', 'or', 'not', 'until', 'eventually'.\n"
|
26 |
+
"Example:\n"
|
27 |
+
"\"overall_consistency\": [\"person holds hotdog\", \"person walks\", \"car next to truck\"]"
|
28 |
+
),
|
29 |
+
Mode.SPATIAL_RELATIONSHIP: (
|
30 |
+
"\"spatial_relationships\":\n"
|
31 |
+
"Extract only spatial relationships between tangible objects (e.g., \"object A next to object B\"). Do not infer or hallucinate spatial relationships.\n"
|
32 |
+
"Example:\n"
|
33 |
+
"\"spatial_relationships\": [\"car next to truck\"]"
|
34 |
+
)
|
35 |
+
}
|
36 |
+
|
37 |
+
mode_outputs = {
|
38 |
+
Mode.OBJECT_ACTION_ALIGNMENT: (
|
39 |
+
" \"object_action_alignment\": [...],\n"
|
40 |
+
" \"object_action_alignment_spec\": \"...\","
|
41 |
+
),
|
42 |
+
Mode.OBJECT_EXISTENCE: (
|
43 |
+
" \"object_existence\": [...],\n"
|
44 |
+
" \"object_existence_spec\": \"...\","
|
45 |
+
),
|
46 |
+
Mode.OVERALL_CONSISTENCY: (
|
47 |
+
" \"overall_consistency\": [...],\n"
|
48 |
+
" \"overall_consistency_spec\": \"...\","
|
49 |
+
),
|
50 |
+
Mode.SPATIAL_RELATIONSHIP: (
|
51 |
+
" \"spatial_relationships\": [...],\n"
|
52 |
+
" \"spatial_relationships_spec\": \"...\""
|
53 |
+
)
|
54 |
+
}
|
55 |
+
|
56 |
+
header = (
|
57 |
+
"You are an intelligent agent designed to extract structured representations from video description prompts. "
|
58 |
+
"You will operate in two stages: (1) proposition extraction and (2) TL specification generation.\n\n"
|
59 |
+
)
|
60 |
+
|
61 |
+
stage1_intro = (
|
62 |
+
"Stage 1: Proposition Extraction\n\n"
|
63 |
+
"Given an input prompt summarizing a video, extract atomic propositions in the following four modes. "
|
64 |
+
"Return all outputs in JSON format.\n\n"
|
65 |
+
)
|
66 |
+
|
67 |
+
stage2_intro = "Stage 2: TL Specification Generation\n\n"
|
68 |
+
|
69 |
+
spec_gen_intro = (
|
70 |
+
"For each of the {n} list(s) of propositions extracted in Stage 1, generate a separate Temporal Logic (TL) specification "
|
71 |
+
"describing the structure or sequence of events in that list.\n\n"
|
72 |
+
)
|
73 |
+
|
74 |
+
tl_instructions = (
|
75 |
+
"Rules for TL specification:\n"
|
76 |
+
"- The input is a single list of propositions from one of the extraction modes.\n"
|
77 |
+
"- The output is a single TL formula using **only** the propositions from that list and the allowed TL symbols: "
|
78 |
+
"['AND', 'OR', 'NOT', 'UNTIL', 'ALWAYS', 'EVENTUALLY']\n"
|
79 |
+
"- Do not introduce any new propositions.\n"
|
80 |
+
"- Each formula should reflect the temporal or logical relationships between the propositions in a way that makes semantic sense.\n\n"
|
81 |
+
)
|
82 |
+
|
83 |
+
input_template = "Input:\n{{\n \"prompt\": \"{}\"\n}}\n\n"
|
84 |
+
expected_output_header = "Expected Output:\n{\n"
|
85 |
+
expected_output_footer = "\n}"
|
neus_v/puls/puls.py
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from llm import *
|
2 |
+
from openai import OpenAI
|
3 |
+
from prompts import *
|
4 |
+
import json
|
5 |
+
import os
|
6 |
+
import re
|
7 |
+
|
8 |
+
def clean_and_parse_json(raw_str):
|
9 |
+
start = raw_str.find('{')
|
10 |
+
end = raw_str.rfind('}') + 1
|
11 |
+
json_str = raw_str[start:end]
|
12 |
+
return json.loads(json_str)
|
13 |
+
|
14 |
+
def process_specification(specification, propositions):
|
15 |
+
new_propositions = []
|
16 |
+
for prop in propositions:
|
17 |
+
prop_cleaned = re.sub(r"^[^a-zA-Z]+|[^a-zA-Z]+$", "", prop)
|
18 |
+
prop_cleaned = re.sub(r"\s+", "_", prop_cleaned)
|
19 |
+
new_propositions.append(prop_cleaned)
|
20 |
+
|
21 |
+
for original, new in zip(propositions, new_propositions):
|
22 |
+
specification = specification.replace(original, f'"{new}"')
|
23 |
+
|
24 |
+
replacements = {
|
25 |
+
"AND": "&",
|
26 |
+
"OR": "|",
|
27 |
+
"UNTIL": "U",
|
28 |
+
"ALWAYS": "G",
|
29 |
+
"EVENTUALLY": "F",
|
30 |
+
"NOT": "!"
|
31 |
+
}
|
32 |
+
for word, symbol in replacements.items():
|
33 |
+
specification = specification.replace(word, symbol)
|
34 |
+
|
35 |
+
return new_propositions, specification
|
36 |
+
|
37 |
+
def create_prompt(prompt, modes):
|
38 |
+
full_prompt = header + stage1_intro
|
39 |
+
|
40 |
+
for i, m in enumerate(modes, start=1):
|
41 |
+
full_prompt += f"{i}. {mode_prompts[m]}\n\n"
|
42 |
+
|
43 |
+
full_prompt += stage2_intro
|
44 |
+
full_prompt += spec_gen_intro.format(n=len(modes))
|
45 |
+
full_prompt += tl_instructions
|
46 |
+
full_prompt += input_template.format(prompt)
|
47 |
+
|
48 |
+
full_prompt += expected_output_header
|
49 |
+
for i, m in enumerate(modes):
|
50 |
+
full_prompt += mode_outputs[m]
|
51 |
+
if i != len(modes) - 1:
|
52 |
+
full_prompt += "\n\n"
|
53 |
+
full_prompt += expected_output_footer
|
54 |
+
|
55 |
+
return full_prompt
|
56 |
+
|
57 |
+
def PULS(prompt, modes, openai_key=None):
|
58 |
+
if openai_key:
|
59 |
+
os.environ["OPENAI_API_KEY"] = openai_key
|
60 |
+
|
61 |
+
client = OpenAI()
|
62 |
+
llm = LLM(client)
|
63 |
+
|
64 |
+
full_prompt = create_prompt(prompt, modes)
|
65 |
+
llm_output = llm.prompt(full_prompt)
|
66 |
+
parsed = clean_and_parse_json(llm_output)
|
67 |
+
|
68 |
+
final_output = {}
|
69 |
+
|
70 |
+
for key, value in parsed.items():
|
71 |
+
if key.endswith("_spec"):
|
72 |
+
base_key = key.replace("_spec", "")
|
73 |
+
propositions = parsed.get(base_key, [])
|
74 |
+
cleaned_props, processed_spec = process_specification(value, propositions)
|
75 |
+
final_output[base_key] = cleaned_props
|
76 |
+
final_output[key] = processed_spec
|
77 |
+
else:
|
78 |
+
final_output[key] = value
|
79 |
+
|
80 |
+
return final_output
|