agentscope-runtime 1.0.1__py3-none-any.whl → 1.0.2__py3-none-any.whl

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 (58) hide show
  1. agentscope_runtime/adapters/agentscope/message.py +32 -7
  2. agentscope_runtime/adapters/agentscope/stream.py +121 -91
  3. agentscope_runtime/adapters/agno/__init__.py +0 -0
  4. agentscope_runtime/adapters/agno/message.py +30 -0
  5. agentscope_runtime/adapters/agno/stream.py +122 -0
  6. agentscope_runtime/adapters/langgraph/__init__.py +12 -0
  7. agentscope_runtime/adapters/langgraph/message.py +257 -0
  8. agentscope_runtime/adapters/langgraph/stream.py +205 -0
  9. agentscope_runtime/cli/__init__.py +7 -0
  10. agentscope_runtime/cli/cli.py +63 -0
  11. agentscope_runtime/cli/commands/__init__.py +2 -0
  12. agentscope_runtime/cli/commands/chat.py +815 -0
  13. agentscope_runtime/cli/commands/deploy.py +1062 -0
  14. agentscope_runtime/cli/commands/invoke.py +58 -0
  15. agentscope_runtime/cli/commands/list_cmd.py +103 -0
  16. agentscope_runtime/cli/commands/run.py +176 -0
  17. agentscope_runtime/cli/commands/sandbox.py +128 -0
  18. agentscope_runtime/cli/commands/status.py +60 -0
  19. agentscope_runtime/cli/commands/stop.py +185 -0
  20. agentscope_runtime/cli/commands/web.py +166 -0
  21. agentscope_runtime/cli/loaders/__init__.py +6 -0
  22. agentscope_runtime/cli/loaders/agent_loader.py +295 -0
  23. agentscope_runtime/cli/state/__init__.py +10 -0
  24. agentscope_runtime/cli/utils/__init__.py +18 -0
  25. agentscope_runtime/cli/utils/console.py +378 -0
  26. agentscope_runtime/cli/utils/validators.py +118 -0
  27. agentscope_runtime/engine/app/agent_app.py +7 -4
  28. agentscope_runtime/engine/deployers/__init__.py +1 -0
  29. agentscope_runtime/engine/deployers/agentrun_deployer.py +152 -22
  30. agentscope_runtime/engine/deployers/base.py +27 -2
  31. agentscope_runtime/engine/deployers/kubernetes_deployer.py +158 -31
  32. agentscope_runtime/engine/deployers/local_deployer.py +188 -25
  33. agentscope_runtime/engine/deployers/modelstudio_deployer.py +109 -18
  34. agentscope_runtime/engine/deployers/state/__init__.py +9 -0
  35. agentscope_runtime/engine/deployers/state/manager.py +388 -0
  36. agentscope_runtime/engine/deployers/state/schema.py +96 -0
  37. agentscope_runtime/engine/deployers/utils/build_cache.py +736 -0
  38. agentscope_runtime/engine/deployers/utils/detached_app.py +105 -30
  39. agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +31 -10
  40. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +15 -8
  41. agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +30 -2
  42. agentscope_runtime/engine/deployers/utils/k8s_utils.py +241 -0
  43. agentscope_runtime/engine/deployers/utils/package.py +56 -6
  44. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +16 -2
  45. agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +155 -5
  46. agentscope_runtime/engine/deployers/utils/wheel_packager.py +107 -123
  47. agentscope_runtime/engine/runner.py +25 -6
  48. agentscope_runtime/engine/schemas/exception.py +580 -0
  49. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +113 -39
  50. agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +20 -4
  51. agentscope_runtime/sandbox/utils.py +2 -0
  52. agentscope_runtime/version.py +1 -1
  53. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/METADATA +24 -7
  54. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/RECORD +58 -28
  55. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/entry_points.txt +1 -0
  56. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/WHEEL +0 -0
  57. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/licenses/LICENSE +0 -0
  58. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,815 @@
1
+ # -*- coding: utf-8 -*-
2
+ """agentscope chat command - Interactive and single-shot agent execution."""
3
+ # pylint: disable=no-value-for-parameter, too-many-branches, protected-access
4
+ # pylint: disable=too-many-statements, too-many-nested-blocks
5
+ # pylint: disable=too-many-nested-blocks, unused-argument
6
+ # pylint: disable=too-many-boolean-expressions
7
+
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ import os
13
+ import signal
14
+ import sys
15
+ from typing import Optional
16
+ from urllib.parse import urljoin
17
+
18
+ import click
19
+ import requests
20
+ import shortuuid
21
+
22
+ from agentscope_runtime.cli.loaders.agent_loader import (
23
+ UnifiedAgentLoader,
24
+ AgentLoadError,
25
+ )
26
+ from agentscope_runtime.cli.utils.validators import validate_agent_source
27
+ from agentscope_runtime.engine.deployers.state import DeploymentStateManager
28
+ from agentscope_runtime.cli.utils.console import (
29
+ echo_error,
30
+ echo_info,
31
+ echo_success,
32
+ echo_warning,
33
+ )
34
+ from agentscope_runtime.engine.schemas.agent_schemas import (
35
+ AgentRequest,
36
+ Message,
37
+ TextContent,
38
+ Role,
39
+ ContentType,
40
+ MessageType,
41
+ )
42
+
43
+
44
+ @click.command()
45
+ @click.argument("source", required=True)
46
+ @click.option(
47
+ "--query",
48
+ "-q",
49
+ help="Single query to execute (non-interactive mode)",
50
+ default=None,
51
+ )
52
+ @click.option(
53
+ "--session-id",
54
+ help="Session ID for conversation continuity",
55
+ default=None,
56
+ )
57
+ @click.option(
58
+ "--user-id",
59
+ help="User ID for the session",
60
+ default="default_user",
61
+ )
62
+ @click.option(
63
+ "--verbose",
64
+ "-v",
65
+ is_flag=True,
66
+ help="Show verbose output including logs and reasoning",
67
+ default=False,
68
+ )
69
+ @click.option(
70
+ "--entrypoint",
71
+ "-e",
72
+ help="Entrypoint file name for directory sources (e.g., 'app.py', "
73
+ "'main.py')",
74
+ default=None,
75
+ )
76
+ def chat(
77
+ source: str,
78
+ query: Optional[str],
79
+ session_id: Optional[str],
80
+ user_id: str,
81
+ verbose: bool,
82
+ entrypoint: Optional[str],
83
+ ):
84
+ """
85
+ Run agent interactively or execute a single query.
86
+
87
+ SOURCE can be:
88
+ \b
89
+ - Path to Python file (e.g., agent.py)
90
+ - Path to project directory (e.g., ./my-agent)
91
+ - Deployment ID (e.g., local_20250101_120000_abc123)
92
+
93
+ Examples:
94
+ \b
95
+ # Interactive mode
96
+ $ agentscope chat agent.py
97
+
98
+ # Single query
99
+ $ agentscope chat agent.py --query "Hello, how are you?"
100
+
101
+ # Use deployment
102
+ $ agentscope chat local_20250101_120000_abc123 --session-id my-session
103
+
104
+ # Verbose mode (show reasoning and logs)
105
+ $ agentscope chat agent.py --query "Hello" --verbose
106
+
107
+ # Use custom entrypoint for directory source
108
+ $ agentscope chat ./my-project --entrypoint custom_app.py
109
+ """
110
+ # Configure logging and tracing based on verbose flag
111
+ if not verbose:
112
+ # Disable console tracing output (JSON logs)
113
+ os.environ["TRACE_ENABLE_LOG"] = "false"
114
+ # Set root logger to WARNING to suppress INFO logs
115
+ logging.getLogger().setLevel(logging.WARNING)
116
+ # Also suppress specific library loggers
117
+ logging.getLogger("agentscope").setLevel(logging.WARNING)
118
+ logging.getLogger("agentscope_runtime").setLevel(logging.WARNING)
119
+ else:
120
+ # Enable console tracing output for verbose mode
121
+ os.environ["TRACE_ENABLE_LOG"] = "true"
122
+ # Set root logger to DEBUG for verbose output
123
+ logging.getLogger().setLevel(logging.DEBUG)
124
+
125
+ try:
126
+ # Initialize state manager
127
+ state_manager = DeploymentStateManager()
128
+ # Check if source is a deployment ID
129
+ try:
130
+ source_type, normalized_source = validate_agent_source(source)
131
+ except Exception:
132
+ # If validation fails, treat as file/directory
133
+ source_type = None
134
+
135
+ if source_type == "deployment_id":
136
+ # Handle deployment ID - use HTTP request
137
+ deployment = state_manager.get(normalized_source)
138
+ if deployment is None:
139
+ echo_error(f"Deployment not found: {normalized_source}")
140
+ sys.exit(1)
141
+
142
+ # Check if platform is modelstudio
143
+ if deployment.platform == "modelstudio":
144
+ echo_warning(
145
+ "ModelStudio deployments do not support query the url. "
146
+ "Please goto the ModelStudio console URL to interact with "
147
+ "the deployment: {deployment.url}",
148
+ )
149
+ sys.exit(1)
150
+
151
+ # Get URL and token
152
+ base_url = deployment.url
153
+ token = deployment.token
154
+
155
+ if not base_url:
156
+ echo_error(
157
+ f"Deployment {normalized_source} does not have a URL "
158
+ f"configured",
159
+ )
160
+ sys.exit(1)
161
+
162
+ # Build process endpoint URL
163
+ # Ensure base_url ends with / for proper urljoin behavior
164
+ base_url_normalized = base_url.rstrip("/") + "/"
165
+ process_url = urljoin(base_url_normalized, "process")
166
+
167
+ # Generate session ID if not provided
168
+ if session_id is None:
169
+ session_id = (
170
+ f"session_{shortuuid.ShortUUID().random(length=8)}"
171
+ )
172
+ echo_info(f"Generated session ID: {session_id}")
173
+
174
+ echo_info(f"Using deployment: {normalized_source}")
175
+ echo_info(f"Endpoint: {process_url}")
176
+
177
+ # Run HTTP-based operations
178
+ if query:
179
+ # Single-shot mode
180
+ _execute_single_query_http(
181
+ process_url,
182
+ token,
183
+ query,
184
+ session_id,
185
+ user_id,
186
+ verbose,
187
+ )
188
+ else:
189
+ # Interactive mode
190
+ _interactive_mode_http(
191
+ process_url,
192
+ token,
193
+ session_id,
194
+ user_id,
195
+ verbose,
196
+ )
197
+ else:
198
+ # Handle file/directory source - use local agent loading
199
+ echo_info(f"Loading agent from: {source}")
200
+ loader = UnifiedAgentLoader(state_manager=state_manager)
201
+
202
+ try:
203
+ agent_app = loader.load(source, entrypoint=entrypoint)
204
+ echo_success("Agent loaded successfully")
205
+ except AgentLoadError as e:
206
+ echo_error(f"Failed to load agent: {e}")
207
+ sys.exit(1)
208
+
209
+ # Generate session ID if not provided
210
+ if session_id is None:
211
+ session_id = (
212
+ f"session_{shortuuid.ShortUUID().random(length=8)}"
213
+ )
214
+ echo_info(f"Generated session ID: {session_id}")
215
+
216
+ # Build runner
217
+ agent_app._build_runner()
218
+ runner = agent_app._runner
219
+
220
+ # Run async operations
221
+ if query:
222
+ # Single-shot mode
223
+ asyncio.run(
224
+ _execute_single_query(
225
+ runner,
226
+ query,
227
+ session_id,
228
+ user_id,
229
+ verbose,
230
+ ),
231
+ )
232
+ else:
233
+ # Interactive mode
234
+ asyncio.run(
235
+ _interactive_mode(runner, session_id, user_id, verbose),
236
+ )
237
+
238
+ except KeyboardInterrupt:
239
+ echo_warning("\nInterrupted by user")
240
+ sys.exit(0)
241
+ except Exception as e:
242
+ echo_error(f"Unexpected error: {e}")
243
+ import traceback
244
+
245
+ traceback.print_exc()
246
+ sys.exit(1)
247
+
248
+
249
+ async def _execute_single_query(
250
+ runner,
251
+ query: str,
252
+ session_id: str,
253
+ user_id: str,
254
+ verbose: bool,
255
+ ):
256
+ """Execute a single query and print response."""
257
+ echo_info(f"Query: {query}")
258
+ echo_info("Response:")
259
+
260
+ # Create Message object for AgentRequest
261
+ user_message = Message(
262
+ role=Role.USER,
263
+ content=[TextContent(text=query)],
264
+ )
265
+
266
+ request = AgentRequest(
267
+ input=[user_message],
268
+ session_id=session_id,
269
+ user_id=user_id,
270
+ )
271
+
272
+ try:
273
+ # Start runner and execute query
274
+ async with runner:
275
+ # Track reasoning message IDs to filter out their content
276
+ reasoning_msg_ids = set()
277
+
278
+ # Use stream_query which handles framework adaptation
279
+ async for event in runner.stream_query(request):
280
+ # Track reasoning messages
281
+ if (
282
+ hasattr(event, "object")
283
+ and event.object == "message"
284
+ and hasattr(event, "type")
285
+ and event.type == MessageType.REASONING
286
+ and hasattr(event, "id")
287
+ ):
288
+ reasoning_msg_ids.add(event.id)
289
+ # Skip reasoning messages in non-verbose mode
290
+ if not verbose:
291
+ continue
292
+
293
+ # Handle streaming content deltas (primary method for
294
+ # streaming)
295
+ if (
296
+ hasattr(event, "object")
297
+ and event.object == "content"
298
+ and hasattr(event, "delta")
299
+ and event.delta is True
300
+ and hasattr(event, "type")
301
+ and event.type == ContentType.TEXT
302
+ and hasattr(event, "text")
303
+ and event.text
304
+ ):
305
+ # Skip content from reasoning messages in non-verbose mode
306
+ if (
307
+ not verbose
308
+ and hasattr(event, "msg_id")
309
+ and event.msg_id in reasoning_msg_ids
310
+ ):
311
+ continue
312
+ print(event.text, end="", flush=True)
313
+ continue
314
+
315
+ # Handle completed messages (fallback for non-streaming
316
+ # responses)
317
+ if hasattr(event, "output") and event.output:
318
+ # This is a response with messages
319
+ for message in event.output:
320
+ # Filter out reasoning messages in non-verbose mode
321
+ if (
322
+ not verbose
323
+ and hasattr(message, "type")
324
+ and message.type == "reasoning"
325
+ ):
326
+ continue
327
+
328
+ if hasattr(message, "content") and message.content:
329
+ # Extract text from content
330
+ for content_item in message.content:
331
+ if (
332
+ hasattr(content_item, "text")
333
+ and content_item.text
334
+ # Only print if this is not a delta (
335
+ # already printed)
336
+ and not (
337
+ hasattr(content_item, "delta")
338
+ and content_item.delta
339
+ )
340
+ ):
341
+ print(
342
+ content_item.text,
343
+ end="",
344
+ flush=True,
345
+ )
346
+
347
+ print() # New line after response
348
+ echo_success("Query completed")
349
+
350
+ except Exception as e:
351
+ echo_error(f"Query failed: {e}")
352
+ raise
353
+
354
+
355
+ async def _interactive_mode(
356
+ runner,
357
+ session_id: str,
358
+ user_id: str,
359
+ verbose: bool,
360
+ ):
361
+ """Run interactive REPL mode."""
362
+ echo_success(
363
+ "Entering interactive mode. Type 'exit' or 'quit' to leave, Ctrl+C "
364
+ "to interrupt.",
365
+ )
366
+ echo_info(f"Session ID: {session_id}")
367
+ echo_info(f"User ID: {user_id}")
368
+ print()
369
+
370
+ # Set up signal handler for Ctrl+C during input
371
+ def handle_sigint(signum, frame):
372
+ """Handle SIGINT (Ctrl+C) gracefully."""
373
+ print() # New line after ^C
374
+ echo_warning(
375
+ "Interrupted. Type 'exit' to quit or continue chatting.",
376
+ )
377
+ # Raise KeyboardInterrupt to be caught by the exception handler
378
+ raise KeyboardInterrupt
379
+
380
+ # Install signal handler
381
+ original_handler = signal.signal(signal.SIGINT, handle_sigint)
382
+
383
+ # Start runner once for the entire interactive session
384
+ async with runner:
385
+ try:
386
+ while True:
387
+ try:
388
+ # Read user input with error handling for encoding issues
389
+ try:
390
+ user_input = input("> ").strip()
391
+ except UnicodeDecodeError as e:
392
+ echo_error(f"Input encoding error: {e}")
393
+ echo_warning(
394
+ "Please ensure your terminal supports UTF-8 "
395
+ "encoding",
396
+ )
397
+ continue
398
+
399
+ if not user_input:
400
+ continue
401
+
402
+ if user_input.lower() in ["exit", "quit", "q"]:
403
+ echo_info("Exiting interactive mode...")
404
+ break
405
+
406
+ # Create Message object
407
+ user_message = Message(
408
+ role=Role.USER,
409
+ content=[TextContent(text=user_input)],
410
+ )
411
+
412
+ # Create request
413
+ request = AgentRequest(
414
+ input=[user_message],
415
+ session_id=session_id,
416
+ user_id=user_id,
417
+ )
418
+
419
+ # Execute query using stream_query
420
+ # Track reasoning message IDs to filter out their content
421
+ reasoning_msg_ids = set()
422
+
423
+ async for event in runner.stream_query(request):
424
+ # Track reasoning messages
425
+ if (
426
+ hasattr(event, "object")
427
+ and event.object == "message"
428
+ and hasattr(event, "type")
429
+ and event.type == MessageType.REASONING
430
+ and hasattr(event, "id")
431
+ ):
432
+ reasoning_msg_ids.add(event.id)
433
+ # Skip reasoning messages in non-verbose mode
434
+ if not verbose:
435
+ continue
436
+
437
+ # Handle streaming content deltas (primary method
438
+ # for streaming)
439
+ if (
440
+ hasattr(event, "object")
441
+ and event.object == "content"
442
+ and hasattr(event, "delta")
443
+ and event.delta is True
444
+ and hasattr(event, "type")
445
+ and event.type == ContentType.TEXT
446
+ and hasattr(event, "text")
447
+ and event.text
448
+ ):
449
+ # Skip content from reasoning messages in
450
+ # non-verbose mode
451
+ if (
452
+ not verbose
453
+ and hasattr(event, "msg_id")
454
+ and event.msg_id in reasoning_msg_ids
455
+ ):
456
+ continue
457
+ print(event.text, end="", flush=True)
458
+ continue
459
+
460
+ # Handle completed messages (fallback for
461
+ # non-streaming responses)
462
+ if hasattr(event, "output") and event.output:
463
+ # This is a response with messages
464
+ for message in event.output:
465
+ # Filter out reasoning in non-verbose mode
466
+ if (
467
+ not verbose
468
+ and hasattr(message, "type")
469
+ and message.type == "reasoning"
470
+ ):
471
+ continue
472
+
473
+ if (
474
+ hasattr(message, "content")
475
+ and message.content
476
+ ):
477
+ # Extract text from content
478
+ for content_item in message.content:
479
+ if (
480
+ hasattr(content_item, "text")
481
+ and content_item.text
482
+ # Only print if this is not a
483
+ # delta (already printed)
484
+ and not (
485
+ hasattr(content_item, "delta")
486
+ and content_item.delta
487
+ )
488
+ ):
489
+ print(
490
+ content_item.text,
491
+ end="",
492
+ flush=True,
493
+ )
494
+
495
+ print() # New line after response
496
+
497
+ except KeyboardInterrupt:
498
+ # Handled by signal handler, just continue
499
+ continue
500
+ except EOFError:
501
+ print()
502
+ echo_info("EOF received. Exiting...")
503
+ break
504
+ except Exception as e:
505
+ # Catch any other unexpected errors
506
+ echo_error(f"\nUnexpected error: {e}")
507
+ import traceback
508
+
509
+ if verbose:
510
+ traceback.print_exc()
511
+ continue
512
+ finally:
513
+ # Restore original signal handler
514
+ signal.signal(signal.SIGINT, original_handler)
515
+
516
+
517
+ def _parse_sse_line(line: bytes) -> tuple[Optional[str], Optional[str]]:
518
+ """Parse a single SSE line."""
519
+ line_str = line.decode("utf-8").strip()
520
+ if line_str.startswith("data: "):
521
+ return "data", line_str[6:]
522
+ elif line_str.startswith("event:"):
523
+ return "event", line_str[7:].strip()
524
+ elif line_str.startswith("id: "):
525
+ return "id", line_str[4:].strip()
526
+ elif line_str.startswith("retry:"):
527
+ return "retry", line_str[7:].strip()
528
+ return None, None
529
+
530
+
531
+ def _execute_single_query_http(
532
+ url: str,
533
+ token: Optional[str],
534
+ query: str,
535
+ session_id: str,
536
+ user_id: str,
537
+ verbose: bool,
538
+ ):
539
+ """Execute a single query via HTTP and print response."""
540
+ echo_info(f"Query: {query}")
541
+ echo_info("Response:")
542
+
543
+ # Prepare request payload
544
+ payload = {
545
+ "input": [
546
+ {
547
+ "role": "user",
548
+ "content": [
549
+ {
550
+ "type": "text",
551
+ "text": query,
552
+ },
553
+ ],
554
+ },
555
+ ],
556
+ "session_id": session_id,
557
+ }
558
+
559
+ # Prepare headers
560
+ headers = {
561
+ "Content-Type": "application/json",
562
+ "Accept": "text/event-stream",
563
+ "Cache-Control": "no-cache",
564
+ }
565
+ if token:
566
+ headers["Authorization"] = f"Bearer {token}"
567
+
568
+ try:
569
+ # Send POST request with streaming
570
+ response = requests.post(
571
+ url,
572
+ json=payload,
573
+ headers=headers,
574
+ stream=True,
575
+ )
576
+ response.raise_for_status()
577
+
578
+ # Parse SSE stream
579
+ for line in response.iter_lines():
580
+ if line:
581
+ field, value = _parse_sse_line(line)
582
+ if field == "data" and value:
583
+ try:
584
+ data = json.loads(value)
585
+ # Handle different object types
586
+ obj_type = data.get("object")
587
+ status = data.get("status")
588
+
589
+ # Skip reasoning messages in non-verbose mode
590
+ if (
591
+ not verbose
592
+ and obj_type == "message"
593
+ and data.get("type") == "reasoning"
594
+ ):
595
+ continue
596
+
597
+ # Handle content deltas (streaming text)
598
+ if (
599
+ obj_type == "content"
600
+ and data.get("delta") is True
601
+ and data.get("type") == "text"
602
+ and data.get("text")
603
+ ):
604
+ print(data["text"], end="", flush=True)
605
+
606
+ # Handle completed messages (for non-streaming
607
+ # responses)
608
+ # Note: We mainly rely on delta content for streaming,
609
+ # but handle completed messages as fallback
610
+ if (
611
+ obj_type == "message"
612
+ and status == "completed"
613
+ and data.get("type") != "reasoning"
614
+ and data.get("content")
615
+ ):
616
+ for content_item in data["content"]:
617
+ if (
618
+ isinstance(content_item, dict)
619
+ and content_item.get("type") == "text"
620
+ and content_item.get("text")
621
+ # Only print if this is not a delta (
622
+ # already printed)
623
+ and not content_item.get("delta")
624
+ ):
625
+ print(
626
+ content_item["text"],
627
+ end="",
628
+ flush=True,
629
+ )
630
+
631
+ except json.JSONDecodeError:
632
+ # Skip invalid JSON lines
633
+ pass
634
+
635
+ print() # New line after response
636
+ echo_success("Query completed")
637
+
638
+ except requests.exceptions.RequestException as e:
639
+ echo_error(f"HTTP request failed: {e}")
640
+ raise
641
+
642
+
643
+ def _interactive_mode_http(
644
+ url: str,
645
+ token: Optional[str],
646
+ session_id: str,
647
+ user_id: str,
648
+ verbose: bool,
649
+ ):
650
+ """Run interactive REPL mode via HTTP."""
651
+ echo_success(
652
+ "Entering interactive mode. Type 'exit' or 'quit' to leave, Ctrl+C "
653
+ "to interrupt.",
654
+ )
655
+ echo_info(f"Session ID: {session_id}")
656
+ echo_info(f"User ID: {user_id}")
657
+ print()
658
+
659
+ # Set up signal handler for Ctrl+C during input
660
+ def handle_sigint(signum, frame):
661
+ """Handle SIGINT (Ctrl+C) gracefully."""
662
+ print() # New line after ^C
663
+ echo_warning(
664
+ "KeyBoardInterrupted. Type 'exit' to quit or continue chatting.",
665
+ )
666
+ # Raise KeyboardInterrupt to be caught by the exception handler
667
+ raise KeyboardInterrupt
668
+
669
+ # Install signal handler
670
+ original_handler = signal.signal(signal.SIGINT, handle_sigint)
671
+
672
+ try:
673
+ while True:
674
+ try:
675
+ # Read user input with error handling for encoding issues
676
+ try:
677
+ user_input = input("> ").strip()
678
+ except UnicodeDecodeError as e:
679
+ echo_error(f"Input encoding error: {e}")
680
+ echo_warning(
681
+ "Please ensure your terminal supports UTF-8 encoding",
682
+ )
683
+ continue
684
+
685
+ if not user_input:
686
+ continue
687
+
688
+ if user_input.lower() in ["exit", "quit", "q"]:
689
+ echo_info("Exiting interactive mode...")
690
+ break
691
+
692
+ # Prepare request payload
693
+ payload = {
694
+ "input": [
695
+ {
696
+ "role": "user",
697
+ "content": [
698
+ {
699
+ "type": "text",
700
+ "text": user_input,
701
+ },
702
+ ],
703
+ },
704
+ ],
705
+ "session_id": session_id,
706
+ }
707
+
708
+ # Prepare headers
709
+ headers = {
710
+ "Content-Type": "application/json",
711
+ "Accept": "text/event-stream",
712
+ "Cache-Control": "no-cache",
713
+ }
714
+ if token:
715
+ headers["Authorization"] = f"Bearer {token}"
716
+
717
+ # Execute query via HTTP
718
+ try:
719
+ response = requests.post(
720
+ url,
721
+ json=payload,
722
+ headers=headers,
723
+ stream=True,
724
+ )
725
+ response.raise_for_status()
726
+
727
+ # Parse SSE stream
728
+ for line in response.iter_lines():
729
+ if line:
730
+ field, value = _parse_sse_line(line)
731
+ if field == "data" and value:
732
+ try:
733
+ data = json.loads(value)
734
+ # Handle different object types
735
+ obj_type = data.get("object")
736
+ status = data.get("status")
737
+
738
+ # Skip reasoning messages in non-verbose
739
+ # mode
740
+ if (
741
+ not verbose
742
+ and obj_type == "message"
743
+ and data.get("type") == "reasoning"
744
+ ):
745
+ continue
746
+
747
+ # Handle content deltas (streaming text)
748
+ if (
749
+ obj_type == "content"
750
+ and data.get("delta") is True
751
+ and data.get("type") == "text"
752
+ and data.get("text")
753
+ ):
754
+ print(data["text"], end="", flush=True)
755
+
756
+ # Handle completed messages (for
757
+ # non-streaming responses)
758
+ # Note: We mainly rely on delta content for
759
+ # streaming,
760
+ # but handle completed messages as fallback
761
+ if (
762
+ obj_type == "message"
763
+ and status == "completed"
764
+ and data.get("type") != "reasoning"
765
+ and data.get("content")
766
+ ):
767
+ for content_item in data["content"]:
768
+ if (
769
+ isinstance(content_item, dict)
770
+ and content_item.get("type")
771
+ == "text"
772
+ and content_item.get("text")
773
+ # Only print if this is not a
774
+ # delta (already printed)
775
+ and not content_item.get(
776
+ "delta",
777
+ )
778
+ ):
779
+ print(
780
+ content_item["text"],
781
+ end="",
782
+ flush=True,
783
+ )
784
+
785
+ except json.JSONDecodeError:
786
+ # Skip invalid JSON lines
787
+ pass
788
+
789
+ print() # New line after response
790
+
791
+ except requests.exceptions.RequestException as e:
792
+ echo_error(f"\nQuery failed: {e}")
793
+
794
+ except KeyboardInterrupt:
795
+ # Handled by signal handler, just continue
796
+ continue
797
+ except EOFError:
798
+ print()
799
+ echo_info("EOF received. Exiting...")
800
+ break
801
+ except Exception as e:
802
+ # Catch any other unexpected errors
803
+ echo_error(f"\nUnexpected error: {e}")
804
+ import traceback
805
+
806
+ if verbose:
807
+ traceback.print_exc()
808
+ continue
809
+ finally:
810
+ # Restore original signal handler
811
+ signal.signal(signal.SIGINT, original_handler)
812
+
813
+
814
+ if __name__ == "__main__":
815
+ chat()