epsimo-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/SKILL.md +85 -0
- package/assets/example_asset.txt +24 -0
- package/epsimo/__init__.py +3 -0
- package/epsimo/__main__.py +4 -0
- package/epsimo/auth.py +143 -0
- package/epsimo/cli.py +586 -0
- package/epsimo/client.py +53 -0
- package/epsimo/resources/assistants.py +47 -0
- package/epsimo/resources/credits.py +16 -0
- package/epsimo/resources/db.py +31 -0
- package/epsimo/resources/files.py +39 -0
- package/epsimo/resources/projects.py +30 -0
- package/epsimo/resources/threads.py +83 -0
- package/epsimo/templates/components/AuthModal/AuthModal.module.css +39 -0
- package/epsimo/templates/components/AuthModal/AuthModal.tsx +138 -0
- package/epsimo/templates/components/BuyCredits/BuyCreditsModal.module.css +96 -0
- package/epsimo/templates/components/BuyCredits/BuyCreditsModal.tsx +132 -0
- package/epsimo/templates/components/BuyCredits/CreditsDisplay.tsx +101 -0
- package/epsimo/templates/components/ThreadChat/ThreadChat.module.css +551 -0
- package/epsimo/templates/components/ThreadChat/ThreadChat.tsx +862 -0
- package/epsimo/templates/components/ThreadChat/components/ToolRenderers.module.css +509 -0
- package/epsimo/templates/components/ThreadChat/components/ToolRenderers.tsx +322 -0
- package/epsimo/templates/next-mvp/app/globals.css.tmpl +20 -0
- package/epsimo/templates/next-mvp/app/layout.tsx.tmpl +22 -0
- package/epsimo/templates/next-mvp/app/page.module.css.tmpl +84 -0
- package/epsimo/templates/next-mvp/app/page.tsx.tmpl +43 -0
- package/epsimo/templates/next-mvp/epsimo.yaml.tmpl +12 -0
- package/epsimo/templates/next-mvp/package.json.tmpl +26 -0
- package/epsimo/tools/library.yaml +51 -0
- package/package.json +27 -0
- package/references/api_reference.md +34 -0
- package/references/virtual_db_guide.md +57 -0
- package/requirements.txt +2 -0
- package/scripts/assistant.py +165 -0
- package/scripts/auth.py +195 -0
- package/scripts/credits.py +107 -0
- package/scripts/debug_run.py +41 -0
- package/scripts/example.py +19 -0
- package/scripts/files.py +73 -0
- package/scripts/find_thread.py +55 -0
- package/scripts/project.py +60 -0
- package/scripts/run.py +75 -0
- package/scripts/test_all_skills.py +387 -0
- package/scripts/test_sdk.py +83 -0
- package/scripts/test_streaming.py +167 -0
- package/scripts/test_vdb.py +65 -0
- package/scripts/thread.py +77 -0
- package/scripts/verify_skill.py +87 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
from auth import get_token, API_BASE_URL
|
|
5
|
+
|
|
6
|
+
target_thread_id = "2b4c6325-c400-4e78-81c8-ef7781bf8b49"
|
|
7
|
+
|
|
8
|
+
def check_thread():
|
|
9
|
+
token = get_token()
|
|
10
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
11
|
+
|
|
12
|
+
print(f"Checking thread {target_thread_id} with User Token...")
|
|
13
|
+
try:
|
|
14
|
+
resp = requests.get(f"{API_BASE_URL}/threads/{target_thread_id}", headers=headers)
|
|
15
|
+
if resp.ok:
|
|
16
|
+
t = resp.json()
|
|
17
|
+
print(f"FOUND with User Token!")
|
|
18
|
+
print(json.dumps(t, indent=2))
|
|
19
|
+
return
|
|
20
|
+
else:
|
|
21
|
+
print(f"User Token check failed: {resp.status_code}")
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print(f"Error: {e}")
|
|
24
|
+
|
|
25
|
+
# Fallback: check projects
|
|
26
|
+
resp = requests.get(f"{API_BASE_URL}/projects/", headers=headers)
|
|
27
|
+
projects = resp.json()
|
|
28
|
+
|
|
29
|
+
for p in projects:
|
|
30
|
+
pid = p['project_id']
|
|
31
|
+
pname = p['name']
|
|
32
|
+
|
|
33
|
+
# Get project token
|
|
34
|
+
pr = requests.get(f"{API_BASE_URL}/projects/{pid}", headers=headers)
|
|
35
|
+
if not pr.ok:
|
|
36
|
+
continue
|
|
37
|
+
ptoken = pr.json().get('access_token')
|
|
38
|
+
pheaders = {"Authorization": f"Bearer {ptoken}"}
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
resp = requests.get(f"{API_BASE_URL}/threads/{target_thread_id}", headers=pheaders)
|
|
42
|
+
if resp.ok:
|
|
43
|
+
t = resp.json()
|
|
44
|
+
print(f"FOUND in Project: {pname} ({pid})")
|
|
45
|
+
print(json.dumps(t, indent=2))
|
|
46
|
+
return
|
|
47
|
+
else:
|
|
48
|
+
pass # Not in this project
|
|
49
|
+
except:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
print("Thread not found by ID.")
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
check_thread()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
import requests
|
|
6
|
+
import json
|
|
7
|
+
from auth import get_token, API_BASE_URL
|
|
8
|
+
|
|
9
|
+
def list_projects():
|
|
10
|
+
token = get_token()
|
|
11
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
12
|
+
response = requests.get(f"{API_BASE_URL}/projects/", headers=headers)
|
|
13
|
+
response.raise_for_status()
|
|
14
|
+
projects = response.json()
|
|
15
|
+
print(json.dumps(projects, indent=2))
|
|
16
|
+
|
|
17
|
+
def create_project(name, description):
|
|
18
|
+
token = get_token()
|
|
19
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
20
|
+
payload = {"name": name, "description": description}
|
|
21
|
+
response = requests.post(f"{API_BASE_URL}/projects/", headers=headers, json=payload)
|
|
22
|
+
if not response.ok:
|
|
23
|
+
print(f"Error {response.status_code}: {response.text}")
|
|
24
|
+
response.raise_for_status()
|
|
25
|
+
print(json.dumps(response.json(), indent=2))
|
|
26
|
+
|
|
27
|
+
def get_project(project_id):
|
|
28
|
+
token = get_token()
|
|
29
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
30
|
+
response = requests.get(f"{API_BASE_URL}/projects/{project_id}", headers=headers)
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
print(json.dumps(response.json(), indent=2))
|
|
33
|
+
|
|
34
|
+
def main():
|
|
35
|
+
parser = argparse.ArgumentParser(description="Manage EpsimoAI Projects")
|
|
36
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
37
|
+
|
|
38
|
+
# List
|
|
39
|
+
subparsers.add_parser("list", help="List all projects")
|
|
40
|
+
|
|
41
|
+
# Get
|
|
42
|
+
get_parser = subparsers.add_parser("get", help="Get project details")
|
|
43
|
+
get_parser.add_argument("--id", required=True, help="Project ID")
|
|
44
|
+
|
|
45
|
+
# Create
|
|
46
|
+
create_parser = subparsers.add_parser("create", help="Create a new project")
|
|
47
|
+
create_parser.add_argument("--name", required=True, help="Project name")
|
|
48
|
+
create_parser.add_argument("--description", help="Project description", default="My AI Project")
|
|
49
|
+
|
|
50
|
+
args = parser.parse_args()
|
|
51
|
+
|
|
52
|
+
if args.command == "list":
|
|
53
|
+
list_projects()
|
|
54
|
+
elif args.command == "get":
|
|
55
|
+
get_project(args.id)
|
|
56
|
+
elif args.command == "create":
|
|
57
|
+
create_project(args.name, args.description)
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
package/scripts/run.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
import requests
|
|
6
|
+
import json
|
|
7
|
+
from auth import get_token, get_project_token, API_BASE_URL
|
|
8
|
+
|
|
9
|
+
def stream_run(project_id, thread_id, message, assistant_id=None):
|
|
10
|
+
token = get_token()
|
|
11
|
+
|
|
12
|
+
# Get project specific token
|
|
13
|
+
project_token = get_project_token(project_id)
|
|
14
|
+
|
|
15
|
+
headers = {
|
|
16
|
+
"Authorization": f"Bearer {project_token}",
|
|
17
|
+
"Accept": "text/event-stream"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Matches frontend structure in ThreadsPage.handleSendMessage
|
|
21
|
+
payload = {
|
|
22
|
+
"thread_id": thread_id,
|
|
23
|
+
"input": [{
|
|
24
|
+
"content": message,
|
|
25
|
+
"type": "human",
|
|
26
|
+
"role": "user"
|
|
27
|
+
}],
|
|
28
|
+
"stream_mode": ["messages", "events", "values"]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Frontend doesn't pass assistant_id here, but we can if API supports override.
|
|
32
|
+
# Frontend relies on thread <-> assistant link.
|
|
33
|
+
if assistant_id:
|
|
34
|
+
payload["assistant_id"] = assistant_id
|
|
35
|
+
|
|
36
|
+
response = requests.post(f"{API_BASE_URL}/runs/stream", headers=headers, json=payload, stream=True)
|
|
37
|
+
|
|
38
|
+
if not response.ok:
|
|
39
|
+
print(f"Status: {response.status_code}")
|
|
40
|
+
print(f"Body: {response.text}")
|
|
41
|
+
|
|
42
|
+
response.raise_for_status()
|
|
43
|
+
|
|
44
|
+
print("Streaming response...")
|
|
45
|
+
for line in response.iter_lines():
|
|
46
|
+
if line:
|
|
47
|
+
decoded_line = line.decode('utf-8')
|
|
48
|
+
if decoded_line.startswith("data:"):
|
|
49
|
+
data_str = decoded_line[5:].strip()
|
|
50
|
+
if data_str == "[DONE]":
|
|
51
|
+
break
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(data_str)
|
|
54
|
+
print(json.dumps(data, indent=None))
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
print(f"Raw: {data_str}")
|
|
57
|
+
|
|
58
|
+
def main():
|
|
59
|
+
parser = argparse.ArgumentParser(description="Manage EpsimoAI Runs")
|
|
60
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
61
|
+
|
|
62
|
+
# Stream
|
|
63
|
+
stream_parser = subparsers.add_parser("stream", help="Stream a run")
|
|
64
|
+
stream_parser.add_argument("--project-id", required=True, help="Project ID")
|
|
65
|
+
stream_parser.add_argument("--thread-id", required=True, help="Thread ID")
|
|
66
|
+
stream_parser.add_argument("--message", required=True, help="User message")
|
|
67
|
+
stream_parser.add_argument("--assistant-id", help="Optional Assistant ID override (default: uses thread's assistant)")
|
|
68
|
+
|
|
69
|
+
args = parser.parse_args()
|
|
70
|
+
|
|
71
|
+
if args.command == "stream":
|
|
72
|
+
stream_run(args.project_id, args.thread_id, args.message, args.assistant_id)
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import requests
|
|
7
|
+
import uuid
|
|
8
|
+
from auth import get_token, get_project_token, API_BASE_URL
|
|
9
|
+
|
|
10
|
+
# --- Colors for Output ---
|
|
11
|
+
class Colors:
|
|
12
|
+
HEADER = '\033[95m'
|
|
13
|
+
OKBLUE = '\033[94m'
|
|
14
|
+
OKCYAN = '\033[96m'
|
|
15
|
+
OKGREEN = '\033[92m'
|
|
16
|
+
WARNING = '\033[93m'
|
|
17
|
+
FAIL = '\033[91m'
|
|
18
|
+
ENDC = '\033[0m'
|
|
19
|
+
BOLD = '\033[1m'
|
|
20
|
+
UNDERLINE = '\033[4m'
|
|
21
|
+
|
|
22
|
+
def print_pass(msg):
|
|
23
|
+
print(f"{Colors.OKGREEN}✅ {msg}{Colors.ENDC}")
|
|
24
|
+
|
|
25
|
+
def print_fail(msg):
|
|
26
|
+
print(f"{Colors.FAIL}❌ {msg}{Colors.ENDC}")
|
|
27
|
+
|
|
28
|
+
def print_info(msg):
|
|
29
|
+
print(f"{Colors.OKCYAN}ℹ️ {msg}{Colors.ENDC}")
|
|
30
|
+
|
|
31
|
+
def print_header(msg):
|
|
32
|
+
print(f"\n{Colors.HEADER}{Colors.BOLD}=== {msg} ==={Colors.ENDC}")
|
|
33
|
+
|
|
34
|
+
class SkillTester:
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self.access_token = None
|
|
37
|
+
self.project_id = None
|
|
38
|
+
self.project_token = None
|
|
39
|
+
self.assistant_id = None
|
|
40
|
+
self.thread_id = None
|
|
41
|
+
self.file_path = "test_secret.txt"
|
|
42
|
+
self.secret_code = f"TEST-SECRET-{uuid.uuid4().hex[:8]}"
|
|
43
|
+
|
|
44
|
+
def step_1_auth(self):
|
|
45
|
+
print_header("Step 1: Authentication")
|
|
46
|
+
try:
|
|
47
|
+
self.access_token = get_token()
|
|
48
|
+
if not self.access_token:
|
|
49
|
+
raise ValueError("No token returned")
|
|
50
|
+
print_pass("Authentication successful")
|
|
51
|
+
|
|
52
|
+
# Check Balance (Credits)
|
|
53
|
+
headers = {"Authorization": f"Bearer {self.access_token}"}
|
|
54
|
+
resp = requests.get(f"{API_BASE_URL}/auth/thread-info", headers=headers)
|
|
55
|
+
if resp.ok:
|
|
56
|
+
data = resp.json()
|
|
57
|
+
print_info(f"Threads Used: {data.get('thread_counter')}/{data.get('thread_max')}")
|
|
58
|
+
else:
|
|
59
|
+
print_fail("Could not fetch balance")
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print_fail(f"Auth failed: {e}")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
def step_2_projects(self):
|
|
66
|
+
print_header("Step 2: Project Management")
|
|
67
|
+
|
|
68
|
+
# Create
|
|
69
|
+
unique_name = f"AutoTest Project {int(time.time())}"
|
|
70
|
+
print_info(f"Creating project: {unique_name}")
|
|
71
|
+
|
|
72
|
+
headers = {"Authorization": f"Bearer {self.access_token}"}
|
|
73
|
+
payload = {
|
|
74
|
+
"name": unique_name,
|
|
75
|
+
"description": "Automated test project"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
resp = requests.post(f"{API_BASE_URL}/projects/", headers=headers, json=payload)
|
|
80
|
+
if not resp.ok:
|
|
81
|
+
raise ValueError(f"Project creation failed: {resp.text}")
|
|
82
|
+
|
|
83
|
+
data = resp.json()
|
|
84
|
+
self.project_id = data["project_id"]
|
|
85
|
+
print_pass(f"Created project {self.project_id}")
|
|
86
|
+
|
|
87
|
+
# Get Project Token
|
|
88
|
+
self.project_token = get_project_token(self.project_id)
|
|
89
|
+
print_pass("Retrieved project-specific token")
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print_fail(f"Project step failed: {e}")
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
def step_3_assistants(self):
|
|
96
|
+
print_header("Step 3: Assistant Management")
|
|
97
|
+
|
|
98
|
+
headers = {"Authorization": f"Bearer {self.project_token}"}
|
|
99
|
+
|
|
100
|
+
# Create
|
|
101
|
+
name = f"AutoTest Assistant {uuid.uuid4().hex[:4]}"
|
|
102
|
+
|
|
103
|
+
# Configuration matches updated assistant.py logic
|
|
104
|
+
instructions = "You are a test assistant. You HAVE access to uploaded files. Use the Retrieval tool to find the secret code."
|
|
105
|
+
|
|
106
|
+
configurable = {
|
|
107
|
+
"type": "agent",
|
|
108
|
+
"type==agent/agent_type": "GPT-4O",
|
|
109
|
+
"type==agent/llm_type": "gpt-4o",
|
|
110
|
+
"type==agent/model": "gpt-4o",
|
|
111
|
+
"type==agent/system_message": instructions,
|
|
112
|
+
"type==agent/tools": [
|
|
113
|
+
{
|
|
114
|
+
"id": f"retrieval-{uuid.uuid4().hex[:4]}",
|
|
115
|
+
"name": "Retrieval",
|
|
116
|
+
"type": "retrieval",
|
|
117
|
+
"description": "Look up information in uploaded files.",
|
|
118
|
+
"config": {}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": f"ddg-{uuid.uuid4().hex[:4]}",
|
|
122
|
+
"name": "DuckDuckGo Search",
|
|
123
|
+
"type": "ddg_search",
|
|
124
|
+
"description": "Search web",
|
|
125
|
+
"config": {}
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
payload = {
|
|
131
|
+
"name": name,
|
|
132
|
+
"config": {
|
|
133
|
+
"configurable": configurable
|
|
134
|
+
},
|
|
135
|
+
"public": True
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
print_info(f"Creating assistant: {name}")
|
|
140
|
+
resp = requests.post(f"{API_BASE_URL}/assistants/", headers=headers, json=payload)
|
|
141
|
+
if not resp.ok:
|
|
142
|
+
raise ValueError(f"Create assistant failed: {resp.text}")
|
|
143
|
+
|
|
144
|
+
data = resp.json()
|
|
145
|
+
self.assistant_id = data["assistant_id"]
|
|
146
|
+
print_pass(f"Created assistant {self.assistant_id}")
|
|
147
|
+
|
|
148
|
+
# List to verify
|
|
149
|
+
resp_list = requests.get(f"{API_BASE_URL}/assistants/", headers=headers)
|
|
150
|
+
if not resp_list.ok:
|
|
151
|
+
raise ValueError("List assistants failed")
|
|
152
|
+
|
|
153
|
+
assistants = resp_list.json()
|
|
154
|
+
found = any(a["assistant_id"] == self.assistant_id for a in assistants)
|
|
155
|
+
if found:
|
|
156
|
+
print_pass("Verification: Assistant found in list")
|
|
157
|
+
else:
|
|
158
|
+
print_fail("Verification: Assistant NOT found in list")
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print_fail(f"Assistant step failed: {e}")
|
|
162
|
+
# Don't exit, might be able to cleanup other things
|
|
163
|
+
|
|
164
|
+
def step_4_files(self):
|
|
165
|
+
print_header("Step 4: File Management")
|
|
166
|
+
if not self.assistant_id:
|
|
167
|
+
print_fail("Skipping files (no assistant)")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Create dummy file
|
|
171
|
+
with open(self.file_path, "w") as f:
|
|
172
|
+
f.write(f"CONFIDENTIAL DATA\nThe secret code is: {self.secret_code}")
|
|
173
|
+
|
|
174
|
+
headers = {"Authorization": f"Bearer {self.project_token}"}
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
print_info(f"Uploading file with secret: {self.secret_code}")
|
|
178
|
+
with open(self.file_path, 'rb') as f:
|
|
179
|
+
files = {'files': (os.path.basename(self.file_path), f)}
|
|
180
|
+
resp = requests.post(f"{API_BASE_URL}/assistants/{self.assistant_id}/files", headers=headers, files=files)
|
|
181
|
+
|
|
182
|
+
if not resp.ok:
|
|
183
|
+
raise ValueError(f"Upload failed: {resp.text}")
|
|
184
|
+
|
|
185
|
+
print_pass("File uploaded successfully")
|
|
186
|
+
|
|
187
|
+
# List files
|
|
188
|
+
resp_list = requests.get(f"{API_BASE_URL}/assistants/{self.assistant_id}/files", headers=headers)
|
|
189
|
+
if resp_list.ok:
|
|
190
|
+
files_list = resp_list.json()
|
|
191
|
+
if len(files_list) > 0:
|
|
192
|
+
print_pass(f"Verified {len(files_list)} file(s) attached")
|
|
193
|
+
else:
|
|
194
|
+
print_fail("File list empty")
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
print_fail(f"File step failed: {e}")
|
|
198
|
+
finally:
|
|
199
|
+
if os.path.exists(self.file_path):
|
|
200
|
+
os.remove(self.file_path)
|
|
201
|
+
|
|
202
|
+
def step_5_threads(self):
|
|
203
|
+
print_header("Step 5: Thread Management")
|
|
204
|
+
if not self.project_token:
|
|
205
|
+
print_fail("Skipping threads (no project token)")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
headers = {"Authorization": f"Bearer {self.project_token}"}
|
|
209
|
+
name = f"Test Thread {uuid.uuid4().hex[:4]}"
|
|
210
|
+
|
|
211
|
+
payload = {
|
|
212
|
+
"name": name,
|
|
213
|
+
"metadata": {"type": "thread"},
|
|
214
|
+
"assistant_id": self.assistant_id
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
print_info(f"Creating thread: {name}")
|
|
219
|
+
resp = requests.post(f"{API_BASE_URL}/threads/", headers=headers, json=payload)
|
|
220
|
+
if not resp.ok:
|
|
221
|
+
raise ValueError(f"Create thread failed: {resp.text}")
|
|
222
|
+
|
|
223
|
+
data = resp.json()
|
|
224
|
+
self.thread_id = data["thread_id"]
|
|
225
|
+
print_pass(f"Created thread {self.thread_id}")
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
print_fail(f"Thread step failed: {e}")
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
|
|
231
|
+
def step_6_execution(self):
|
|
232
|
+
print_header("Step 6: Execution (Runs & RAG)")
|
|
233
|
+
if not self.thread_id or not self.assistant_id:
|
|
234
|
+
print_fail("Skipping execution (missing resources)")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
headers = {
|
|
238
|
+
"Authorization": f"Bearer {self.project_token}",
|
|
239
|
+
"Accept": "text/event-stream"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
payload = {
|
|
243
|
+
"thread_id": self.thread_id,
|
|
244
|
+
"assistant_id": self.assistant_id,
|
|
245
|
+
"input": [{
|
|
246
|
+
"content": "What is the secret code in the file?",
|
|
247
|
+
"type": "human",
|
|
248
|
+
"role": "user"
|
|
249
|
+
}],
|
|
250
|
+
"stream_mode": ["messages", "values"]
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
print_info("Streaming run request...")
|
|
254
|
+
found_secret = False
|
|
255
|
+
full_response = ""
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
resp = requests.post(f"{API_BASE_URL}/runs/stream", headers=headers, json=payload, stream=True)
|
|
259
|
+
if not resp.ok:
|
|
260
|
+
raise ValueError(f"Stream failed: {resp.status_code} {resp.text}")
|
|
261
|
+
|
|
262
|
+
for line in resp.iter_lines():
|
|
263
|
+
if line:
|
|
264
|
+
decoded = line.decode('utf-8')
|
|
265
|
+
if decoded.startswith("data:"):
|
|
266
|
+
data_str = decoded[5:].strip()
|
|
267
|
+
if data_str == "[DONE]":
|
|
268
|
+
break
|
|
269
|
+
try:
|
|
270
|
+
# Try to extract content delta or message
|
|
271
|
+
# This is a simplified check, actual structure varies
|
|
272
|
+
if self.secret_code in data_str:
|
|
273
|
+
found_secret = True
|
|
274
|
+
full_response += data_str
|
|
275
|
+
except:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
if found_secret:
|
|
279
|
+
print_pass(f"RAG Successful! Secret '{self.secret_code}' found in stream.")
|
|
280
|
+
else:
|
|
281
|
+
# Polling retry logic for latency
|
|
282
|
+
max_retries = 6
|
|
283
|
+
for i in range(max_retries):
|
|
284
|
+
wait_time = 10
|
|
285
|
+
print_info(f"Secret not found. Retrying in {wait_time}s ({i+1}/{max_retries})...")
|
|
286
|
+
time.sleep(wait_time)
|
|
287
|
+
|
|
288
|
+
full_response = ""
|
|
289
|
+
current_run_found = False
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
resp = requests.post(f"{API_BASE_URL}/runs/stream", headers=headers, json=payload, stream=True)
|
|
293
|
+
for line in resp.iter_lines():
|
|
294
|
+
if line:
|
|
295
|
+
decoded = line.decode('utf-8')
|
|
296
|
+
if decoded.startswith("data:"):
|
|
297
|
+
data_str = decoded[5:].strip()
|
|
298
|
+
if data_str == "[DONE]":
|
|
299
|
+
break
|
|
300
|
+
try:
|
|
301
|
+
if self.secret_code in data_str:
|
|
302
|
+
current_run_found = True
|
|
303
|
+
full_response += data_str
|
|
304
|
+
except:
|
|
305
|
+
pass
|
|
306
|
+
except Exception as e:
|
|
307
|
+
print_fail(f"Retry {i+1} failed with error: {e}")
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
if current_run_found:
|
|
311
|
+
found_secret = True
|
|
312
|
+
print_pass(f"RAG Successful (on retry {i+1})! Secret '{self.secret_code}' found.")
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
if not found_secret:
|
|
316
|
+
print_fail("Secret not found after retries.")
|
|
317
|
+
print_info(f"Last response dump (truncated): {full_response[:200]}...")
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
print_fail(f"Execution step failed: {e}")
|
|
321
|
+
|
|
322
|
+
def step_7_credits_check(self):
|
|
323
|
+
print_header("Step 7: Credits Purchase (Dry Run)")
|
|
324
|
+
# Just verify we can generate a checkout link
|
|
325
|
+
if not self.access_token:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
headers = {
|
|
329
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
330
|
+
"Content-Type": "application/json"
|
|
331
|
+
}
|
|
332
|
+
payload = {"quantity": 100, "total_amount": 10.0}
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
resp = requests.post(f"{API_BASE_URL}/checkout/create-checkout-session", json=payload, headers=headers)
|
|
336
|
+
if resp.ok:
|
|
337
|
+
url = resp.json().get("url")
|
|
338
|
+
if url:
|
|
339
|
+
print_pass("Checkout session URL generated successfully")
|
|
340
|
+
else:
|
|
341
|
+
print_fail("No URL in checkout response")
|
|
342
|
+
else:
|
|
343
|
+
print_fail(f"Checkout creation failed: {resp.text}")
|
|
344
|
+
except Exception as e:
|
|
345
|
+
print_fail(f"Credits step failed: {e}")
|
|
346
|
+
|
|
347
|
+
def step_8_cleanup(self):
|
|
348
|
+
print_header("Step 8: Cleanup")
|
|
349
|
+
# cleanup is important
|
|
350
|
+
|
|
351
|
+
headers = {"Authorization": f"Bearer {self.access_token}"} # Use main token for project deletion context switching
|
|
352
|
+
|
|
353
|
+
if self.project_id:
|
|
354
|
+
# We need to delete the project.
|
|
355
|
+
# Note: Deleting project *should* delete assistants/threads/files inside it.
|
|
356
|
+
try:
|
|
357
|
+
# Switch context explicitly just in case or use main token? api.d.ts says delete project uses /projects/{id}
|
|
358
|
+
# And usually requires just access to it.
|
|
359
|
+
|
|
360
|
+
# Check api.d.ts: delete_project_projects__project_id__delete
|
|
361
|
+
# If confirm=true is needed
|
|
362
|
+
print_info(f"Deleting project {self.project_id}...")
|
|
363
|
+
resp = requests.delete(f"{API_BASE_URL}/projects/{self.project_id}?confirm=true", headers=headers)
|
|
364
|
+
|
|
365
|
+
if resp.ok:
|
|
366
|
+
print_pass("Project deleted successfully")
|
|
367
|
+
else:
|
|
368
|
+
print_fail(f"Project deletion failed: {resp.status_code} {resp.text}")
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
print_fail(f"Cleanup failed: {e}")
|
|
372
|
+
|
|
373
|
+
def run(self):
|
|
374
|
+
print_header("🚀 STARTING COMPREHENSIVE EPSIMO SKILL TEST 🚀")
|
|
375
|
+
self.step_1_auth()
|
|
376
|
+
self.step_2_projects()
|
|
377
|
+
self.step_3_assistants()
|
|
378
|
+
self.step_4_files()
|
|
379
|
+
self.step_5_threads()
|
|
380
|
+
self.step_6_execution()
|
|
381
|
+
self.step_7_credits_check()
|
|
382
|
+
self.step_8_cleanup()
|
|
383
|
+
print_header("🏁 TEST SUITE COMPLETE 🏁")
|
|
384
|
+
|
|
385
|
+
if __name__ == "__main__":
|
|
386
|
+
tester = SkillTester()
|
|
387
|
+
tester.run()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
# Add the current directory to sys.path so we can import 'epsimo' and 'scripts'
|
|
6
|
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
7
|
+
|
|
8
|
+
from epsimo import EpsimoClient
|
|
9
|
+
# Reuse auth helper to get a token for testing
|
|
10
|
+
from scripts.auth import get_token
|
|
11
|
+
|
|
12
|
+
def test_sdk():
|
|
13
|
+
print("🚀 Testing Epsimo Python SDK...")
|
|
14
|
+
|
|
15
|
+
# Authenticate
|
|
16
|
+
token = get_token()
|
|
17
|
+
client = EpsimoClient(api_key=token)
|
|
18
|
+
print("✅ Client initialized")
|
|
19
|
+
|
|
20
|
+
# 1. Create Project
|
|
21
|
+
p_name = f"SDK Test {int(time.time())}"
|
|
22
|
+
print(f"Creating project '{p_name}'...")
|
|
23
|
+
project = client.projects.create(name=p_name, description="SDK Test Project")
|
|
24
|
+
p_id = project["project_id"]
|
|
25
|
+
print(f"✅ Project Created: {p_id}")
|
|
26
|
+
|
|
27
|
+
# 2. Create Assistant
|
|
28
|
+
print("Creating assistant...")
|
|
29
|
+
asst = client.assistants.create(
|
|
30
|
+
project_id=p_id,
|
|
31
|
+
name="SDK Bot",
|
|
32
|
+
instructions="You are a bot created via the SDK.",
|
|
33
|
+
model="gpt-4o"
|
|
34
|
+
)
|
|
35
|
+
a_id = asst["assistant_id"]
|
|
36
|
+
print(f"✅ Assistant Created: {a_id}")
|
|
37
|
+
|
|
38
|
+
# 3. Create Thread
|
|
39
|
+
print("Creating thread...")
|
|
40
|
+
thread = client.threads.create(project_id=p_id, name="SDK Thread", assistant_id=a_id)
|
|
41
|
+
t_id = thread["thread_id"]
|
|
42
|
+
print(f"✅ Thread Created: {t_id}")
|
|
43
|
+
|
|
44
|
+
# 4. Stream Run
|
|
45
|
+
print("Streaming run...")
|
|
46
|
+
print("--- Output ---")
|
|
47
|
+
stream = client.threads.run_stream(
|
|
48
|
+
project_id=p_id,
|
|
49
|
+
thread_id=t_id,
|
|
50
|
+
assistant_id=a_id,
|
|
51
|
+
message="Say 'Hello from SDK'"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
full_text = ""
|
|
55
|
+
for chunk in stream:
|
|
56
|
+
# print(f"DEBUG CHUNK: {chunk}")
|
|
57
|
+
content_found = None
|
|
58
|
+
|
|
59
|
+
if isinstance(chunk, list):
|
|
60
|
+
for item in chunk:
|
|
61
|
+
if isinstance(item, dict) and "content" in item:
|
|
62
|
+
content_found = item["content"]
|
|
63
|
+
break # Usually one message per chunk update in this mode
|
|
64
|
+
elif isinstance(chunk, dict) and "content" in chunk:
|
|
65
|
+
content_found = chunk["content"]
|
|
66
|
+
|
|
67
|
+
if content_found:
|
|
68
|
+
sys.stdout.write(content_found)
|
|
69
|
+
full_text += content_found
|
|
70
|
+
|
|
71
|
+
print("\n--- End Output ---")
|
|
72
|
+
if "Hello from SDK" in full_text:
|
|
73
|
+
print("✅ Streaming Success")
|
|
74
|
+
else:
|
|
75
|
+
print("⚠️ Streaming finished but expected text not found.")
|
|
76
|
+
|
|
77
|
+
# 5. Cleanup
|
|
78
|
+
print("Cleaning up...")
|
|
79
|
+
client.projects.delete(p_id, confirm=True)
|
|
80
|
+
print("✅ Project Deleted")
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
test_sdk()
|