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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/SKILL.md +85 -0
  3. package/assets/example_asset.txt +24 -0
  4. package/epsimo/__init__.py +3 -0
  5. package/epsimo/__main__.py +4 -0
  6. package/epsimo/auth.py +143 -0
  7. package/epsimo/cli.py +586 -0
  8. package/epsimo/client.py +53 -0
  9. package/epsimo/resources/assistants.py +47 -0
  10. package/epsimo/resources/credits.py +16 -0
  11. package/epsimo/resources/db.py +31 -0
  12. package/epsimo/resources/files.py +39 -0
  13. package/epsimo/resources/projects.py +30 -0
  14. package/epsimo/resources/threads.py +83 -0
  15. package/epsimo/templates/components/AuthModal/AuthModal.module.css +39 -0
  16. package/epsimo/templates/components/AuthModal/AuthModal.tsx +138 -0
  17. package/epsimo/templates/components/BuyCredits/BuyCreditsModal.module.css +96 -0
  18. package/epsimo/templates/components/BuyCredits/BuyCreditsModal.tsx +132 -0
  19. package/epsimo/templates/components/BuyCredits/CreditsDisplay.tsx +101 -0
  20. package/epsimo/templates/components/ThreadChat/ThreadChat.module.css +551 -0
  21. package/epsimo/templates/components/ThreadChat/ThreadChat.tsx +862 -0
  22. package/epsimo/templates/components/ThreadChat/components/ToolRenderers.module.css +509 -0
  23. package/epsimo/templates/components/ThreadChat/components/ToolRenderers.tsx +322 -0
  24. package/epsimo/templates/next-mvp/app/globals.css.tmpl +20 -0
  25. package/epsimo/templates/next-mvp/app/layout.tsx.tmpl +22 -0
  26. package/epsimo/templates/next-mvp/app/page.module.css.tmpl +84 -0
  27. package/epsimo/templates/next-mvp/app/page.tsx.tmpl +43 -0
  28. package/epsimo/templates/next-mvp/epsimo.yaml.tmpl +12 -0
  29. package/epsimo/templates/next-mvp/package.json.tmpl +26 -0
  30. package/epsimo/tools/library.yaml +51 -0
  31. package/package.json +27 -0
  32. package/references/api_reference.md +34 -0
  33. package/references/virtual_db_guide.md +57 -0
  34. package/requirements.txt +2 -0
  35. package/scripts/assistant.py +165 -0
  36. package/scripts/auth.py +195 -0
  37. package/scripts/credits.py +107 -0
  38. package/scripts/debug_run.py +41 -0
  39. package/scripts/example.py +19 -0
  40. package/scripts/files.py +73 -0
  41. package/scripts/find_thread.py +55 -0
  42. package/scripts/project.py +60 -0
  43. package/scripts/run.py +75 -0
  44. package/scripts/test_all_skills.py +387 -0
  45. package/scripts/test_sdk.py +83 -0
  46. package/scripts/test_streaming.py +167 -0
  47. package/scripts/test_vdb.py +65 -0
  48. package/scripts/thread.py +77 -0
  49. package/scripts/verify_skill.py +87 -0
package/epsimo/cli.py ADDED
@@ -0,0 +1,586 @@
1
+ import argparse
2
+ import sys
3
+ import os
4
+ import json
5
+ import yaml
6
+ from .client import EpsimoClient
7
+ from .auth import login_interactive, get_token
8
+
9
+ def cmd_whoami(args):
10
+ """Show current user info."""
11
+ print("👤 Fetching user info...")
12
+ try:
13
+ token = get_token()
14
+ if not token:
15
+ print("❌ Not logged in. Use 'epsimo auth'.")
16
+ return
17
+
18
+ client = EpsimoClient(api_key=token)
19
+ # Using a guessed endpoint from auth scripts (thread-info is a proxy for profile info sometimes)
20
+ # Or better check if there is a profile endpoint?
21
+ # api.d.ts doesn't explicitly show /auth/whoami, but /auth/thread-info is available
22
+ info = client.request("GET", "/auth/thread-info")
23
+ print(f"Logged in as: {info.get('email', 'Unknown User')}")
24
+ print(f"Threads Used: {info.get('thread_counter')}/{info.get('thread_max')}")
25
+
26
+ except Exception as e:
27
+ print(f"❌ Failed to fetch user info: {e}")
28
+
29
+ def cmd_balance(args):
30
+ """Check the current thread and credit balance."""
31
+ print("💳 Checking balance...")
32
+ try:
33
+ token = get_token()
34
+ client = EpsimoClient(api_key=token)
35
+ data = client.credits.get_balance()
36
+
37
+ thread_count = data.get("thread_counter", 0)
38
+ thread_max = data.get("thread_max", 0)
39
+ remaining = thread_max - thread_count
40
+
41
+ print("\n=== Thread Balance ===")
42
+ print(f"Threads Used: {thread_count}")
43
+ print(f"Total Allowance: {thread_max}")
44
+ print(f"Threads Remaining: {remaining}")
45
+ print("======================\n")
46
+
47
+ except Exception as e:
48
+ print(f"❌ Failed to check balance: {e}")
49
+
50
+ def cmd_buy(args):
51
+ """Create a checkout session to buy credits."""
52
+ print(f"🛒 Preparing purchase of {args.quantity} threads...")
53
+
54
+ quantity = args.quantity
55
+ total_amount = args.amount
56
+
57
+ # Estimation logic if amount not provided
58
+ if total_amount is None:
59
+ if quantity >= 1000:
60
+ price_per_unit = 0.08
61
+ elif quantity >= 500:
62
+ price_per_unit = 0.09
63
+ else:
64
+ price_per_unit = 0.10
65
+ total_amount = round(quantity * price_per_unit, 2)
66
+ print(f"ℹ️ Estimated cost: {total_amount} EUR")
67
+
68
+ try:
69
+ token = get_token()
70
+ client = EpsimoClient(api_key=token)
71
+ data = client.credits.create_checkout_session(quantity, total_amount)
72
+
73
+ checkout_url = data.get("url")
74
+ if checkout_url:
75
+ print("\n✅ Checkout session created successfully!")
76
+ print(f"Please visit this URL to complete your purchase:\n\n{checkout_url}\n")
77
+ else:
78
+ print("❌ No checkout URL returned from server.")
79
+
80
+ except Exception as e:
81
+ print(f"❌ Failed to create checkout session: {e}")
82
+
83
+ def cmd_auth(args):
84
+ """Handle authentication."""
85
+ print("🔐 Authenticating Epsimo CLI...")
86
+ token = get_token()
87
+ if token:
88
+ print("ℹ️ Already logged in. Re-authenticating...")
89
+
90
+ try:
91
+ login_interactive()
92
+ token = get_token()
93
+ if token:
94
+ print(f"✅ Successfully logged in!")
95
+ else:
96
+ print("❌ Login failed (no token found).")
97
+ except Exception as e:
98
+ print(f"❌ Error during auth: {e}")
99
+
100
+ def cmd_projects(args):
101
+ """List projects."""
102
+ if not args.json:
103
+ print("📁 Fetching projects...")
104
+ try:
105
+ token = get_token()
106
+ client = EpsimoClient(api_key=token)
107
+ projects = client.projects.list()
108
+
109
+ if args.json:
110
+ print(json.dumps(projects))
111
+ return
112
+
113
+ if not projects:
114
+ print("No projects found.")
115
+ return
116
+
117
+ print(f"{'ID':<40} | {'Name':<20}")
118
+ print("-" * 65)
119
+ for p in projects:
120
+ print(f"{p['project_id']:<40} | {p['name']:<20}")
121
+
122
+ except Exception as e:
123
+ if not args.json:
124
+ print(f"❌ Failed to fetch projects: {e}")
125
+ else:
126
+ print(json.dumps({"error": str(e)}))
127
+
128
+ def cmd_assistants(args):
129
+ """List assistants in a project."""
130
+ if not args.json:
131
+ print(f"🤖 Fetching assistants for project {args.project_id}...")
132
+ try:
133
+ token = get_token()
134
+ client = EpsimoClient(api_key=token)
135
+ assistants = client.assistants.list(args.project_id)
136
+
137
+ if args.json:
138
+ print(json.dumps(assistants))
139
+ return
140
+
141
+ if not assistants:
142
+ print("No assistants found.")
143
+ return
144
+
145
+ print(f"{'ID':<40} | {'Name':<20}")
146
+ print("-" * 65)
147
+ for a in assistants:
148
+ print(f"{a['assistant_id']:<40} | {a['name']:<20}")
149
+
150
+ except Exception as e:
151
+ if not args.json:
152
+ print(f"❌ Failed to fetch assistants: {e}")
153
+ else:
154
+ print(json.dumps({"error": str(e)}))
155
+
156
+ def cmd_threads(args):
157
+ """List threads in a project."""
158
+ print(f"🧵 Fetching threads for project {args.project_id}...")
159
+ try:
160
+ token = get_token()
161
+ client = EpsimoClient(api_key=token)
162
+ threads = client.threads.list(args.project_id)
163
+
164
+ if not threads:
165
+ print("No threads found.")
166
+ return
167
+
168
+ print(f"{'ID':<40} | {'Name':<20}")
169
+ print("-" * 65)
170
+ for t in threads:
171
+ print(f"{t['thread_id']:<40} | {t['name']:<20}")
172
+
173
+ except Exception as e:
174
+ print(f"❌ Failed to fetch threads: {e}")
175
+
176
+ def cmd_init(args):
177
+ """Initialize a new Epsimo project in the current directory."""
178
+ print("🚀 Initializing Epsimo project...")
179
+
180
+ if os.path.exists("epsimo.yaml"):
181
+ print("⚠️ epsimo.yaml already exists in this directory.")
182
+ choice = input("Do you want to overwrite it? (y/n): ")
183
+ if choice.lower() != 'y':
184
+ return
185
+
186
+ # 1. Auth & Client
187
+ try:
188
+ token = get_token()
189
+ if not token:
190
+ print("🔐 Authentication required.")
191
+ login_interactive()
192
+ token = get_token()
193
+
194
+ client = EpsimoClient(api_key=token)
195
+ except Exception as e:
196
+ print(f"❌ Auth failed: {e}")
197
+ return
198
+
199
+ # 2. Project Selection/Creation
200
+ project_name = args.name or os.path.basename(os.getcwd())
201
+ print(f"📁 Project Name: {project_name}")
202
+
203
+ try:
204
+ print("Creating project on Epsimo platform...")
205
+ project = client.projects.create(name=project_name)
206
+ project_id = project["project_id"]
207
+ print(f"✅ Project Created: {project_id}")
208
+ except Exception as e:
209
+ print(f"❌ Failed to create project: {e}")
210
+ return
211
+
212
+ # 3. Generate epsimo.yaml
213
+ config = {
214
+ "project_id": project_id,
215
+ "name": project_name,
216
+ "assistants": [
217
+ {
218
+ "name": "default-assistant",
219
+ "model": "gpt-4o",
220
+ "instructions": "You are a helpful AI assistant created via the Epsimo CLI.",
221
+ "tools": [
222
+ {"type": "retrieval"}
223
+ ]
224
+ }
225
+ ]
226
+ }
227
+
228
+ try:
229
+ with open("epsimo.yaml", "w") as f:
230
+ yaml.dump(config, f, sort_keys=False)
231
+ print("✅ Created epsimo.yaml")
232
+ print("\nNext steps:")
233
+ print("1. Edit epsimo.yaml to configure your assistants.")
234
+ print(f"2. Run 'epsimo deploy' to sync changes (coming soon).")
235
+ print(f"3. Chat with your assistant: 'epsimo run --project-id {project_id} --assistant-id <ID>'")
236
+ except Exception as e:
237
+ print(f"❌ Failed to write epsimo.yaml: {e}")
238
+
239
+ def cmd_deploy(args):
240
+ """Deploy configuration from epsimo.yaml to the platform."""
241
+ print("🚀 Deploying configuration...")
242
+
243
+ if not os.path.exists("epsimo.yaml"):
244
+ print("❌ epsimo.yaml not found. Run 'epsimo init' first.")
245
+ return
246
+
247
+ # 1. Load config
248
+ try:
249
+ with open("epsimo.yaml", "r") as f:
250
+ config = yaml.safe_load(f)
251
+ except Exception as e:
252
+ print(f"❌ Failed to load epsimo.yaml: {e}")
253
+ return
254
+
255
+ project_id = config.get("project_id")
256
+ if not project_id:
257
+ print("❌ project_id missing in epsimo.yaml")
258
+ return
259
+
260
+ # 2. Auth & Client
261
+ try:
262
+ token = get_token()
263
+ client = EpsimoClient(api_key=token)
264
+ except Exception as e:
265
+ print(f"❌ Auth failed: {e}")
266
+ return
267
+
268
+ # 3. Process Assistants
269
+ assistants_config = config.get("assistants", [])
270
+ print(f"📦 Found {len(assistants_config)} assistants in config.")
271
+
272
+ try:
273
+ # Fetch current assistants to match by name
274
+ current_assistants = client.assistants.list(project_id)
275
+ asst_map = {a["name"]: a for a in current_assistants}
276
+
277
+ for asst_cfg in assistants_config:
278
+ name = asst_cfg.get("name")
279
+ if not name: continue
280
+
281
+ model = asst_cfg.get("model", "gpt-4o")
282
+ instructions = asst_cfg.get("instructions", "")
283
+ tools = asst_cfg.get("tools", [])
284
+
285
+ if name in asst_map:
286
+ asst_id = asst_map[name]["assistant_id"]
287
+ print(f"🔄 Updating assistant: {name} ({asst_id})...")
288
+ client.assistants.update(project_id, asst_id, {
289
+ "name": name,
290
+ "config": {
291
+ "configurable": {
292
+ "type": "agent",
293
+ "type==agent/model": model,
294
+ "type==agent/system_message": instructions,
295
+ "type==agent/tools": tools
296
+ }
297
+ }
298
+ })
299
+ else:
300
+ print(f"✨ Creating assistant: {name}...")
301
+ client.assistants.create(
302
+ project_id=project_id,
303
+ name=name,
304
+ model=model,
305
+ instructions=instructions,
306
+ tools=tools
307
+ )
308
+
309
+ print("✅ Deployment complete!")
310
+
311
+ except Exception as e:
312
+ print(f"❌ Deployment failed: {e}")
313
+
314
+ def cmd_db(args):
315
+ """Query the structured state (virtual database) of a thread."""
316
+ print(f"📊 Querying Virtual Database for thread {args.thread_id}...")
317
+ try:
318
+ token = get_token()
319
+ client = EpsimoClient(api_key=token)
320
+ state = client.threads.get_state(args.project_id, args.thread_id)
321
+
322
+ # The state typically has 'values' which is our DB
323
+ values = state.get("values", {})
324
+
325
+ if not values:
326
+ print("📭 Database is empty.")
327
+ return
328
+
329
+ print("\n=== Current State (JSON) ===")
330
+ print(json.dumps(values, indent=2))
331
+ print("============================\n")
332
+
333
+ except Exception as e:
334
+ print(f"❌ Failed to query database: {e}")
335
+
336
+ def cmd_db_set(args):
337
+ """Set a value in the thread's virtual database."""
338
+ print(f"📝 Setting {args.key} = {args.value} in thread {args.thread_id}...")
339
+ try:
340
+ # Parse value as JSON if possible, otherwise keep as string
341
+ try:
342
+ val = json.loads(args.value)
343
+ except:
344
+ val = args.value
345
+
346
+ token = get_token()
347
+ client = EpsimoClient(api_key=token)
348
+ client.threads.set_state(args.project_id, args.thread_id, {args.key: val})
349
+ print("✅ State updated successfully.")
350
+
351
+ except Exception as e:
352
+ print(f"❌ Failed to update database: {e}")
353
+
354
+ def cmd_create(args):
355
+ """Scaffold a new Epsimo MVP project."""
356
+ project_name = args.name
357
+ project_slug = project_name.lower().replace(" ", "-")
358
+ target_dir = os.path.abspath(project_slug)
359
+
360
+ print(f"🏗️ Creating new Epsimo MVP project: {project_name}...")
361
+
362
+ if os.path.exists(target_dir):
363
+ print(f"❌ Directory {project_slug} already exists.")
364
+ return
365
+
366
+ # Path to templates inside the skill
367
+ base_dir = os.path.dirname(os.path.abspath(__file__))
368
+ template_dir = os.path.join(base_dir, "templates", "next-mvp")
369
+
370
+ if not os.path.exists(template_dir):
371
+ print(f"❌ Template directory not found at {template_dir}")
372
+ return
373
+
374
+ try:
375
+ os.makedirs(target_dir)
376
+
377
+ # 1. Walk through template dir and copy/transform files
378
+ for root, dirs, files in os.walk(template_dir):
379
+ rel_path = os.path.relpath(root, template_dir)
380
+
381
+ # Create subdirectories
382
+ for d in dirs:
383
+ os.makedirs(os.path.join(target_dir, rel_path, d), exist_ok=True)
384
+
385
+ # Copy files
386
+ for f in files:
387
+ if not f.endswith(".tmpl"): continue
388
+
389
+ src_path = os.path.join(root, f)
390
+ dest_filename = f.replace(".tmpl", "")
391
+ dest_path = os.path.join(target_dir, rel_path, dest_filename)
392
+
393
+ with open(src_path, "r") as src_f:
394
+ content = src_f.read()
395
+
396
+ # Replace placeholders
397
+ content = content.replace("{{PROJECT_NAME}}", project_name)
398
+ content = content.replace("{{PROJECT_SLUG}}", project_slug)
399
+ content = content.replace("{{ASSISTANT_ID}}", "TODO_DEPLOY_FIRST")
400
+
401
+ os.makedirs(os.path.dirname(dest_path), exist_ok=True)
402
+ with open(dest_path, "w") as dest_f:
403
+ dest_f.write(content)
404
+
405
+ # 2. Copy the UI components from the current codebase to the new project
406
+ # In a real framework, this would be an npm install @epsimo/ui
407
+ # For this MVP, we copy the files.
408
+ print("📦 Injecting Epsimo UI Kit...")
409
+ # Since we are in the context of the current repo:
410
+ src_ui_dir = "/Users/thierry/code/epsimo-frontend/src/components/epsimo"
411
+ dest_ui_dir = os.path.join(target_dir, "components", "epsimo")
412
+
413
+ import shutil
414
+ if os.path.exists(src_ui_dir):
415
+ shutil.copytree(src_ui_dir, dest_ui_dir, dirs_exist_ok=True)
416
+
417
+ print(f"\n✅ Project {project_name} created successfully at ./{project_slug}")
418
+ print("\nNext steps:")
419
+ print(f"1. cd {project_slug}")
420
+ print("2. npm install")
421
+ print("3. epsimo init (to link to platform)")
422
+ print("4. epsimo deploy (to create assistants)")
423
+ print("5. npm run dev")
424
+
425
+ except Exception as e:
426
+ print(f"❌ Failed to create project: {e}")
427
+
428
+ def cmd_run(args):
429
+ """Run an interactive chat with an assistant."""
430
+ print(f"▶️ Running Assistant: {args.assistant_id}")
431
+
432
+ # 1. Initialize Client
433
+ try:
434
+ client = EpsimoClient() # Auto-loads token from file via get_token in auth logic if not passed,
435
+ # BUT client.py currently expects api_key env var or arg.
436
+ # Let's update client to try get_token if nothing passed?
437
+ # For now, explicit:
438
+ token = get_token()
439
+ client = EpsimoClient(api_key=token)
440
+ except Exception as e:
441
+ print(f"❌ Auth failed: {e}. Try 'epsimo auth'.")
442
+ return
443
+
444
+ # 2. Verify Assistant Exists (and get project context implicitly?)
445
+ # We need a project_id to run things.
446
+ # CLI args should probably include project_id or we find the assistant.
447
+ # Finding assistant across all projects is hard without a "search" endpoint.
448
+
449
+ if not args.project_id:
450
+ print("❌ --project-id is currently required.")
451
+ return
452
+
453
+ # 3. Create Thread
454
+ print("🧵 Creating session thread...")
455
+ try:
456
+ thread = client.threads.create(args.project_id, "CLI Session", args.assistant_id)
457
+ thread_id = thread["thread_id"]
458
+ except Exception as e:
459
+ print(f"❌ Failed to create thread: {e}")
460
+ return
461
+
462
+ # 4. Chat Loop
463
+ print(f"✅ Ready! (Thread: {thread_id})")
464
+ print("Type 'exit' to quit.\n")
465
+
466
+ while True:
467
+ try:
468
+ user_input = input("You > ")
469
+ if user_input.lower() in ["exit", "quit"]:
470
+ break
471
+
472
+ print("Bot > ", end="", flush=True)
473
+ stream = client.threads.run_stream(
474
+ project_id=args.project_id,
475
+ thread_id=thread_id,
476
+ assistant_id=args.assistant_id,
477
+ message=user_input
478
+ )
479
+
480
+ for chunk in stream:
481
+ # Handle both list and dict chunks as discovered in testing
482
+ content = None
483
+ if isinstance(chunk, list):
484
+ for item in chunk:
485
+ if isinstance(item, dict) and "content" in item:
486
+ content = item["content"]
487
+ break
488
+ elif isinstance(chunk, dict) and "content" in chunk:
489
+ content = chunk["content"]
490
+
491
+ if content:
492
+ sys.stdout.write(content)
493
+ sys.stdout.flush()
494
+ print("\n")
495
+
496
+ except KeyboardInterrupt:
497
+ break
498
+ except Exception as e:
499
+ print(f"\n❌ Error: {e}")
500
+ break
501
+
502
+ def main():
503
+ parser = argparse.ArgumentParser(description="Epsimo Agent Framework CLI")
504
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
505
+
506
+ # epsimo init
507
+ init_parser = subparsers.add_parser("init", help="Initialize a new project")
508
+ init_parser.add_argument("--name", help="Project name (defaults to current directory)")
509
+ init_parser.set_defaults(func=cmd_init)
510
+
511
+ # epsimo deploy
512
+ deploy_parser = subparsers.add_parser("deploy", help="Deploy config from epsimo.yaml")
513
+ deploy_parser.set_defaults(func=cmd_deploy)
514
+
515
+ # epsimo create
516
+ create_parser = subparsers.add_parser("create", help="Create a new Epsimo MVP project")
517
+ create_parser.add_argument("name", help="Name of the project")
518
+ create_parser.set_defaults(func=cmd_create)
519
+
520
+ # epsimo auth
521
+ auth_parser = subparsers.add_parser("auth", help="Login to Epsimo")
522
+ auth_parser.set_defaults(func=cmd_auth)
523
+
524
+ # epsimo whoami
525
+ whoami_parser = subparsers.add_parser("whoami", help="Show current user info")
526
+ whoami_parser.set_defaults(func=cmd_whoami)
527
+
528
+ # epsimo projects
529
+ projects_parser = subparsers.add_parser("projects", help="List projects")
530
+ projects_parser.add_argument("--json", action="store_true", help="Output as JSON")
531
+ projects_parser.set_defaults(func=cmd_projects)
532
+
533
+ # epsimo credits {balance, buy}
534
+ credits_parser = subparsers.add_parser("credits", help="Manage credits and thread usage")
535
+ credits_subparsers = credits_parser.add_subparsers(dest="credits_command", help="Credits command")
536
+
537
+ balance_parser = credits_subparsers.add_parser("balance", help="Check current credit balance")
538
+ balance_parser.set_defaults(func=cmd_balance)
539
+
540
+ buy_parser = credits_subparsers.add_parser("buy", help="Buy more credits")
541
+ buy_parser.add_argument("--quantity", type=int, required=True, help="Number of threads to purchase")
542
+ buy_parser.add_argument("--amount", type=float, help="Total amount to pay in EUR (optional, calculated if omitted)")
543
+ buy_parser.set_defaults(func=cmd_buy)
544
+
545
+ # epsimo assistants --project-id X
546
+ assistants_parser = subparsers.add_parser("assistants", help="List assistants")
547
+ assistants_parser.add_argument("--project-id", required=True, help="Project ID")
548
+ assistants_parser.add_argument("--json", action="store_true", help="Output as JSON")
549
+ assistants_parser.set_defaults(func=cmd_assistants)
550
+
551
+ # epsimo threads --project-id X
552
+ threads_parser = subparsers.add_parser("threads", help="List threads")
553
+ threads_parser.add_argument("--project-id", required=True, help="Project ID")
554
+ threads_parser.set_defaults(func=cmd_threads)
555
+
556
+ # epsimo run --project-id X --assistant-id Y
557
+ run_parser = subparsers.add_parser("run", help="Run a terminal chat session")
558
+ run_parser.add_argument("--project-id", required=True, help="Project ID")
559
+ run_parser.add_argument("--assistant-id", required=True, help="Assistant ID")
560
+ run_parser.set_defaults(func=cmd_run)
561
+
562
+ # epsimo db query --project-id X --thread-id Y
563
+ db_parser = subparsers.add_parser("db", help="Manage Virtual Database state")
564
+ db_subparsers = db_parser.add_subparsers(dest="db_command", help="DB command")
565
+
566
+ query_parser = db_subparsers.add_parser("query", help="Query the current state of a thread")
567
+ query_parser.add_argument("--project-id", required=True, help="Project ID")
568
+ query_parser.add_argument("--thread-id", required=True, help="Thread ID")
569
+ query_parser.set_defaults(func=cmd_db)
570
+
571
+ set_parser = db_subparsers.add_parser("set", help="Set a value in the thread state")
572
+ set_parser.add_argument("--project-id", required=True, help="Project ID")
573
+ set_parser.add_argument("--thread-id", required=True, help="Thread ID")
574
+ set_parser.add_argument("--key", required=True, help="Key to set")
575
+ set_parser.add_argument("--value", required=True, help="Value to set (JSON strings supported)")
576
+ set_parser.set_defaults(func=cmd_db_set)
577
+
578
+ args = parser.parse_args()
579
+
580
+ if hasattr(args, "func"):
581
+ args.func(args)
582
+ else:
583
+ parser.print_help()
584
+
585
+ if __name__ == "__main__":
586
+ main()
@@ -0,0 +1,53 @@
1
+ import os
2
+ import requests
3
+ from .resources.projects import Projects
4
+ from .resources.assistants import Assistants
5
+ from .resources.threads import Threads
6
+ from .resources.files import Files
7
+ from .resources.credits import Credits
8
+ from .resources.db import Database
9
+
10
+ class EpsimoClient:
11
+ def __init__(self, api_key=None, base_url=None):
12
+ self.api_key = api_key or os.environ.get("EPSIMO_API_KEY")
13
+ self.base_url = base_url or os.environ.get("EPSIMO_API_URL", "https://api.epsimoagents.com")
14
+
15
+ # In the future, we might support API Keys directly.
16
+ # For now, we reuse the JWT token logic but wrapped cleanly.
17
+ # If the user passes a token as api_key, we use it.
18
+ self._session = requests.Session()
19
+ if self.api_key:
20
+ self._session.headers.update({"Authorization": f"Bearer {self.api_key}"})
21
+
22
+ self.projects = Projects(self)
23
+ self.assistants = Assistants(self)
24
+ self.threads = Threads(self)
25
+ self.files = Files(self)
26
+ self.credits = Credits(self)
27
+ self.db = Database(self)
28
+
29
+ def request(self, method, path, **kwargs):
30
+ url = f"{self.base_url}{path}"
31
+ response = self._session.request(method, url, **kwargs)
32
+ if not response.ok:
33
+ try:
34
+ import json
35
+ print(f"❌ API Error ({response.status_code}): {json.dumps(response.json(), indent=2)}")
36
+ except:
37
+ print(f"❌ API Error ({response.status_code}): {response.text}")
38
+ response.raise_for_status()
39
+ if response.status_code == 204:
40
+ return None
41
+ return response.json()
42
+
43
+ def get_project_headers(self, project_id):
44
+ """Fetch/Construct headers including project-specific token."""
45
+ # Using the clean client.projects access
46
+ resp = self.projects.get(project_id)
47
+ token = resp.get("access_token") or resp.get("token") or resp.get("jwt_token")
48
+
49
+ if not token:
50
+ # Just a warning or error?
51
+ print(f"Warning: No specific token found for project {project_id}")
52
+
53
+ return {"Authorization": f"Bearer {token}"}
@@ -0,0 +1,47 @@
1
+ class Assistants:
2
+ def __init__(self, client):
3
+ self.client = client
4
+
5
+ def list(self, project_id):
6
+ """List assistants in a project."""
7
+ headers = self.client.get_project_headers(project_id)
8
+ return self.client.request("GET", "/assistants/", headers=headers)
9
+
10
+ def create(self, project_id, name, model="gpt-4o", instructions="", tools=None, public=False):
11
+ """Create a new assistant."""
12
+ headers = self.client.get_project_headers(project_id)
13
+
14
+ # Construct configurable config
15
+ configurable = {
16
+ "type": "agent",
17
+ "type==agent/agent_type": "GPT-4O", # legacy/specific key
18
+ "type==agent/model": model,
19
+ "type==agent/system_message": instructions,
20
+ }
21
+
22
+ if tools:
23
+ # Normalize tools format if needed, or assume list of dicts
24
+ # The API expects specific structure
25
+ configurable["type==agent/tools"] = tools
26
+
27
+ payload = {
28
+ "name": name,
29
+ "config": {"configurable": configurable},
30
+ "public": public
31
+ }
32
+ return self.client.request("POST", "/assistants/", json=payload, headers=headers)
33
+
34
+ def get(self, project_id, assistant_id):
35
+ """Get assistant details."""
36
+ headers = self.client.get_project_headers(project_id)
37
+ return self.client.request("GET", f"/assistants/{assistant_id}", headers=headers)
38
+
39
+ def update(self, project_id, assistant_id, payload):
40
+ """Update assistant settings."""
41
+ headers = self.client.get_project_headers(project_id)
42
+ return self.client.request("PUT", f"/assistants/{assistant_id}", json=payload, headers=headers)
43
+
44
+ def delete(self, project_id, assistant_id):
45
+ """Delete an assistant."""
46
+ headers = self.client.get_project_headers(project_id)
47
+ return self.client.request("DELETE", f"/assistants/{assistant_id}", headers=headers)