mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-24 03:00:28 -05:00
Compare commits
4 Commits
fix/agent-
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef42b17e3b | ||
|
|
a18ffd0b21 | ||
|
|
e40c8c70ce | ||
|
|
9cdcd6793f |
@@ -1,572 +0,0 @@
|
|||||||
2026-02-21 20:31:19,811 [34mINFO[0m Initializing LaunchDarkly Client 9.15.0
|
|
||||||
2026-02-21 20:31:19,812 [34mINFO[0m Starting event processor
|
|
||||||
2026-02-21 20:31:19,812 [34mINFO[0m Starting StreamingUpdateProcessor connecting to uri: https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:19,812 [34mINFO[0m Waiting up to 5 seconds for LaunchDarkly client to initialize...
|
|
||||||
2026-02-21 20:31:19,812 [34mINFO[0m Connecting to stream at https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:20,051 [34mINFO[0m StreamingUpdateProcessor initialized ok.
|
|
||||||
2026-02-21 20:31:20,051 [34mINFO[0m Started LaunchDarkly Client: OK
|
|
||||||
2026-02-21 20:31:20,051 [34mINFO[0m LaunchDarkly client initialized successfully
|
|
||||||
2026-02-21 20:31:21,578 [33mWARNING[0m [33mProvider LINEAR implements OAuth but the required env vars LINEAR_CLIENT_ID and LINEAR_CLIENT_SECRET are not both set[0m
|
|
||||||
2026-02-21 20:31:21,623 [33mWARNING[0m [33mAuthentication error: Langfuse client initialized without public_key. Client will be disabled. Provide a public_key parameter or set LANGFUSE_PUBLIC_KEY environment variable. [0m
|
|
||||||
2026-02-21 20:31:21,796 [34mINFO[0m Metrics endpoint exposed at /metrics for external-api
|
|
||||||
2026-02-21 20:31:21,800 [34mINFO[0m Metrics endpoint exposed at /metrics for rest-api
|
|
||||||
2026-02-21 20:31:21,881 [34mINFO[0m Metrics endpoint exposed at /metrics for websocket-server
|
|
||||||
2026-02-21 20:31:21,913 [33mWARNING[0m [33mPostmark server API token not found, email sending disabled[0m
|
|
||||||
2026-02-21 20:31:21,956 [34mINFO[0m [DatabaseManager] started with PID 6089
|
|
||||||
2026-02-21 20:31:21,958 [34mINFO[0m [Scheduler] started with PID 6090
|
|
||||||
2026-02-21 20:31:21,959 [34mINFO[0m [NotificationManager] started with PID 6091
|
|
||||||
2026-02-21 20:31:21,960 [34mINFO[0m [WebsocketServer] started with PID 6092
|
|
||||||
2026-02-21 20:31:21,961 [34mINFO[0m [AgentServer] started with PID 6093
|
|
||||||
2026-02-21 20:31:21,962 [34mINFO[0m [ExecutionManager] started with PID 6094
|
|
||||||
2026-02-21 20:31:21,963 [34mINFO[0m [CoPilotExecutor] Starting...
|
|
||||||
2026-02-21 20:31:21,963 [34mINFO[0m [CoPilotExecutor] Pod assigned executor_id: fb7d76b3-8dc3-40a4-947e-a93bfad207da
|
|
||||||
2026-02-21 20:31:21,963 [34mINFO[0m [CoPilotExecutor] Spawn max-5 workers...
|
|
||||||
2026-02-21 20:31:21,970 [34mINFO[0m [PID-6048|THREAD-77685505|CoPilotExecutor|RabbitMQ-124e33d7-4877-4745-9778-6b6b06de92d2] Acquiring connection started...
|
|
||||||
2026-02-21 20:31:21,971 [34mINFO[0m [PID-6048|THREAD-77685506|CoPilotExecutor|RabbitMQ-124e33d7-4877-4745-9778-6b6b06de92d2] Acquiring connection started...
|
|
||||||
2026-02-21 20:31:21,973 [34mINFO[0m Pika version 1.3.2 connecting to ('::1', 5672, 0, 0)
|
|
||||||
2026-02-21 20:31:21,973 [34mINFO[0m Pika version 1.3.2 connecting to ('::1', 5672, 0, 0)
|
|
||||||
2026-02-21 20:31:21,974 [34mINFO[0m Socket connected: <socket.socket fd=30, family=30, type=1, proto=6, laddr=('::1', 55999, 0, 0), raddr=('::1', 5672, 0, 0)>
|
|
||||||
2026-02-21 20:31:21,975 [34mINFO[0m Socket connected: <socket.socket fd=29, family=30, type=1, proto=6, laddr=('::1', 55998, 0, 0), raddr=('::1', 5672, 0, 0)>
|
|
||||||
2026-02-21 20:31:21,975 [34mINFO[0m Streaming transport linked up: (<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120f5eba0>, _StreamingProtocolShim: <SelectConnection PROTOCOL transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120f5eba0> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>).
|
|
||||||
2026-02-21 20:31:21,976 [34mINFO[0m Streaming transport linked up: (<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120fa0410>, _StreamingProtocolShim: <SelectConnection PROTOCOL transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120fa0410> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>).
|
|
||||||
2026-02-21 20:31:21,990 [34mINFO[0m AMQPConnector - reporting success: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120fa0410> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:21,991 [34mINFO[0m AMQPConnectionWorkflow - reporting success: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120fa0410> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:21,991 [34mINFO[0m AMQPConnector - reporting success: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120f5eba0> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:21,991 [34mINFO[0m Connection workflow succeeded: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120fa0410> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:21,991 [34mINFO[0m AMQPConnectionWorkflow - reporting success: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120f5eba0> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:21,991 [34mINFO[0m Created channel=1
|
|
||||||
2026-02-21 20:31:21,992 [34mINFO[0m Connection workflow succeeded: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x120f5eba0> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:21,992 [34mINFO[0m Created channel=1
|
|
||||||
2026-02-21 20:31:22,005 [34mINFO[0m [PID-6048|THREAD-77685505|CoPilotExecutor|RabbitMQ-124e33d7-4877-4745-9778-6b6b06de92d2] Acquiring connection completed successfully.
|
|
||||||
2026-02-21 20:31:22,005 [34mINFO[0m [PID-6048|THREAD-77685506|CoPilotExecutor|RabbitMQ-124e33d7-4877-4745-9778-6b6b06de92d2] Acquiring connection completed successfully.
|
|
||||||
2026-02-21 20:31:22,007 [34mINFO[0m [CoPilotExecutor] Starting to consume cancel messages...
|
|
||||||
2026-02-21 20:31:22,008 [34mINFO[0m [CoPilotExecutor] Starting to consume run messages...
|
|
||||||
2026-02-21 20:31:23,199 [34mINFO[0m Initializing LaunchDarkly Client 9.15.0
|
|
||||||
2026-02-21 20:31:23,201 [34mINFO[0m Starting event processor
|
|
||||||
2026-02-21 20:31:23,202 [34mINFO[0m Starting StreamingUpdateProcessor connecting to uri: https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:23,202 [34mINFO[0m Waiting up to 5 seconds for LaunchDarkly client to initialize...
|
|
||||||
2026-02-21 20:31:23,202 [34mINFO[0m Connecting to stream at https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:23,331 [34mINFO[0m StreamingUpdateProcessor initialized ok.
|
|
||||||
2026-02-21 20:31:23,331 [34mINFO[0m Started LaunchDarkly Client: OK
|
|
||||||
2026-02-21 20:31:23,332 [34mINFO[0m LaunchDarkly client initialized successfully
|
|
||||||
2026-02-21 20:31:23,891 [34mINFO[0m Initializing LaunchDarkly Client 9.15.0
|
|
||||||
2026-02-21 20:31:23,892 [34mINFO[0m Starting event processor
|
|
||||||
2026-02-21 20:31:23,893 [34mINFO[0m Starting StreamingUpdateProcessor connecting to uri: https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:23,893 [34mINFO[0m Waiting up to 5 seconds for LaunchDarkly client to initialize...
|
|
||||||
2026-02-21 20:31:23,893 [34mINFO[0m Connecting to stream at https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:23,946 [34mINFO[0m Initializing LaunchDarkly Client 9.15.0
|
|
||||||
2026-02-21 20:31:23,947 [34mINFO[0m Starting event processor
|
|
||||||
2026-02-21 20:31:23,947 [34mINFO[0m Starting StreamingUpdateProcessor connecting to uri: https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:23,947 [34mINFO[0m Waiting up to 5 seconds for LaunchDarkly client to initialize...
|
|
||||||
2026-02-21 20:31:23,948 [34mINFO[0m Connecting to stream at https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:24,017 [34mINFO[0m StreamingUpdateProcessor initialized ok.
|
|
||||||
2026-02-21 20:31:24,017 [34mINFO[0m Started LaunchDarkly Client: OK
|
|
||||||
2026-02-21 20:31:24,017 [34mINFO[0m LaunchDarkly client initialized successfully
|
|
||||||
2026-02-21 20:31:24,065 [34mINFO[0m StreamingUpdateProcessor initialized ok.
|
|
||||||
2026-02-21 20:31:24,065 [34mINFO[0m Started LaunchDarkly Client: OK
|
|
||||||
2026-02-21 20:31:24,065 [34mINFO[0m LaunchDarkly client initialized successfully
|
|
||||||
2026-02-21 20:31:24,707 [34mINFO[0m [NotificationManager] Starting...
|
|
||||||
2026-02-21 20:31:24,750 [34mINFO[0m Metrics endpoint exposed at /metrics for NotificationManager
|
|
||||||
2026-02-21 20:31:24,754 [34mINFO[0m [PID-6091|THREAD-77685702|NotificationManager|FastAPI server-d17271ed-e3a2-4e93-900b-a0d3bd2b8100] Running FastAPI server started...
|
|
||||||
2026-02-21 20:31:24,755 [34mINFO[0m [NotificationManager] Starting RPC server at http://localhost:8007
|
|
||||||
2026-02-21 20:31:24,756 [34mINFO[0m [NotificationManager] [NotificationManager] ⏳ Configuring RabbitMQ...
|
|
||||||
2026-02-21 20:31:24,757 [34mINFO[0m [PID-6091|THREAD-77685703|NotificationManager|AsyncRabbitMQ-7963c91c-c443-4479-a55e-5e9a8d7d942d] Acquiring async connection started...
|
|
||||||
2026-02-21 20:31:24,775 [34mINFO[0m Started server process [6091]
|
|
||||||
2026-02-21 20:31:24,775 [34mINFO[0m Waiting for application startup.
|
|
||||||
2026-02-21 20:31:24,776 [34mINFO[0m Application startup complete.
|
|
||||||
2026-02-21 20:31:24,777 [31mERROR[0m [31m[Errno 48] error while attempting to bind on address ('::1', 8007, 0, 0): [errno 48] address already in use[0m
|
|
||||||
2026-02-21 20:31:24,781 [34mINFO[0m Waiting for application shutdown.
|
|
||||||
2026-02-21 20:31:24,781 [34mINFO[0m [NotificationManager] ✅ FastAPI has finished
|
|
||||||
2026-02-21 20:31:24,782 [34mINFO[0m Application shutdown complete.
|
|
||||||
2026-02-21 20:31:24,783 [34mINFO[0m [NotificationManager] 🛑 Shared event loop stopped
|
|
||||||
2026-02-21 20:31:24,783 [34mINFO[0m [NotificationManager] 🧹 Running cleanup
|
|
||||||
2026-02-21 20:31:24,783 [34mINFO[0m [NotificationManager] ⏳ Disconnecting RabbitMQ...
|
|
||||||
Process NotificationManager:
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/process.py", line 313, in _bootstrap
|
|
||||||
self.run()
|
|
||||||
~~~~~~~~^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/process.py", line 108, in run
|
|
||||||
self._target(*self._args, **self._kwargs)
|
|
||||||
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/backend/util/process.py", line 83, in execute_run_command
|
|
||||||
self.cleanup()
|
|
||||||
~~~~~~~~~~~~^^
|
|
||||||
File "/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/backend/notifications/notifications.py", line 1094, in cleanup
|
|
||||||
self.run_and_wait(self.rabbitmq_service.disconnect())
|
|
||||||
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/backend/util/service.py", line 136, in run_and_wait
|
|
||||||
return asyncio.run_coroutine_threadsafe(coro, self.shared_event_loop).result()
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/tasks.py", line 1003, in run_coroutine_threadsafe
|
|
||||||
loop.call_soon_threadsafe(callback)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 873, in call_soon_threadsafe
|
|
||||||
self._check_closed()
|
|
||||||
~~~~~~~~~~~~~~~~~~^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 551, in _check_closed
|
|
||||||
raise RuntimeError('Event loop is closed')
|
|
||||||
RuntimeError: Event loop is closed
|
|
||||||
/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/process.py:327: RuntimeWarning: coroutine 'AsyncRabbitMQ.disconnect' was never awaited
|
|
||||||
traceback.print_exc()
|
|
||||||
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
|
|
||||||
2026-02-21 20:31:24,846 [34mINFO[0m Initializing LaunchDarkly Client 9.15.0
|
|
||||||
2026-02-21 20:31:24,848 [34mINFO[0m Starting event processor
|
|
||||||
2026-02-21 20:31:24,848 [34mINFO[0m Starting StreamingUpdateProcessor connecting to uri: https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:24,849 [34mINFO[0m Waiting up to 5 seconds for LaunchDarkly client to initialize...
|
|
||||||
2026-02-21 20:31:24,849 [34mINFO[0m Connecting to stream at https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:24,857 [34mINFO[0m Initializing LaunchDarkly Client 9.15.0
|
|
||||||
2026-02-21 20:31:24,858 [34mINFO[0m Starting event processor
|
|
||||||
2026-02-21 20:31:24,858 [34mINFO[0m Starting StreamingUpdateProcessor connecting to uri: https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:24,858 [34mINFO[0m Waiting up to 5 seconds for LaunchDarkly client to initialize...
|
|
||||||
2026-02-21 20:31:24,858 [34mINFO[0m Connecting to stream at https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:24,862 [34mINFO[0m Initializing LaunchDarkly Client 9.15.0
|
|
||||||
2026-02-21 20:31:24,863 [34mINFO[0m Starting event processor
|
|
||||||
2026-02-21 20:31:24,864 [34mINFO[0m Starting StreamingUpdateProcessor connecting to uri: https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:24,864 [34mINFO[0m Waiting up to 5 seconds for LaunchDarkly client to initialize...
|
|
||||||
2026-02-21 20:31:24,864 [34mINFO[0m Connecting to stream at https://stream.launchdarkly.com/all
|
|
||||||
2026-02-21 20:31:24,966 [34mINFO[0m StreamingUpdateProcessor initialized ok.
|
|
||||||
2026-02-21 20:31:24,967 [34mINFO[0m Started LaunchDarkly Client: OK
|
|
||||||
2026-02-21 20:31:24,967 [34mINFO[0m LaunchDarkly client initialized successfully
|
|
||||||
2026-02-21 20:31:24,976 [34mINFO[0m StreamingUpdateProcessor initialized ok.
|
|
||||||
2026-02-21 20:31:24,976 [34mINFO[0m Started LaunchDarkly Client: OK
|
|
||||||
2026-02-21 20:31:24,976 [34mINFO[0m LaunchDarkly client initialized successfully
|
|
||||||
2026-02-21 20:31:24,989 [34mINFO[0m StreamingUpdateProcessor initialized ok.
|
|
||||||
2026-02-21 20:31:24,989 [34mINFO[0m Started LaunchDarkly Client: OK
|
|
||||||
2026-02-21 20:31:24,989 [34mINFO[0m LaunchDarkly client initialized successfully
|
|
||||||
2026-02-21 20:31:25,035 [34mINFO[0m Metrics endpoint exposed at /metrics for websocket-server
|
|
||||||
2026-02-21 20:31:25,036 [34mINFO[0m [WebsocketServer] Starting...
|
|
||||||
2026-02-21 20:31:25,036 [34mINFO[0m CORS allow origins: ['http://localhost:3000', 'http://127.0.0.1:3000']
|
|
||||||
2026-02-21 20:31:25,076 [34mINFO[0m Started server process [6092]
|
|
||||||
2026-02-21 20:31:25,076 [34mINFO[0m Waiting for application startup.
|
|
||||||
2026-02-21 20:31:25,077 [34mINFO[0m Application startup complete.
|
|
||||||
2026-02-21 20:31:25,077 [34mINFO[0m [PID-6092|THREAD-77685501|WebsocketServer|AsyncRedis-b6fb3c5c-0070-4c5c-90eb-922d4f2152c2] Acquiring connection started...
|
|
||||||
2026-02-21 20:31:25,077 [34mINFO[0m [PID-6092|THREAD-77685501|WebsocketServer|AsyncRedis-b6fb3c5c-0070-4c5c-90eb-922d4f2152c2] Acquiring connection started...
|
|
||||||
2026-02-21 20:31:25,078 [31mERROR[0m [31m[Errno 48] error while attempting to bind on address ('0.0.0.0', 8001): address already in use[0m
|
|
||||||
2026-02-21 20:31:25,080 [34mINFO[0m Waiting for application shutdown.
|
|
||||||
2026-02-21 20:31:25,080 [34mINFO[0m Application shutdown complete.
|
|
||||||
2026-02-21 20:31:25,080 [34mINFO[0m Event broadcaster stopped
|
|
||||||
2026-02-21 20:31:25,081 [33mWARNING[0m [33m[WebsocketServer] 🛑 Terminating because of SystemExit: 1[0m
|
|
||||||
2026-02-21 20:31:25,081 [34mINFO[0m [WebsocketServer] 🧹 Running cleanup
|
|
||||||
2026-02-21 20:31:25,081 [34mINFO[0m [WebsocketServer] ✅ Cleanup done
|
|
||||||
2026-02-21 20:31:25,081 [34mINFO[0m [WebsocketServer] 🛑 Terminated
|
|
||||||
2026-02-21 20:31:25,915 [34mINFO[0m [DatabaseManager] Starting...
|
|
||||||
2026-02-21 20:31:25,947 [34mINFO[0m Metrics endpoint exposed at /metrics for DatabaseManager
|
|
||||||
2026-02-21 20:31:25,970 [34mINFO[0m [ExecutionManager] Starting...
|
|
||||||
2026-02-21 20:31:25,970 [34mINFO[0m [GraphExecutor] [ExecutionManager] 🆔 Pod assigned executor_id: 90ff5962-bdc8-456d-a864-01c5f4f199bd
|
|
||||||
2026-02-21 20:31:25,971 [34mINFO[0m [GraphExecutor] [ExecutionManager] ⏳ Spawn max-10 workers...
|
|
||||||
2026-02-21 20:31:25,973 [34mINFO[0m [Scheduler] Starting...
|
|
||||||
2026-02-21 20:31:25,971 [33mWARNING[0m [33m[ExecutionManager] 🛑 Terminating because of OSError: [Errno 48] Address already in use[0m
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/backend/util/process.py", line 65, in execute_run_command
|
|
||||||
self.run()
|
|
||||||
~~~~~~~~^^
|
|
||||||
File "/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/backend/executor/manager.py", line 1554, in run
|
|
||||||
start_http_server(settings.config.execution_manager_port)
|
|
||||||
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/prometheus_client/exposition.py", line 251, in start_wsgi_server
|
|
||||||
httpd = make_server(addr, port, app, TmpServer, handler_class=_SilentHandler)
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/wsgiref/simple_server.py", line 150, in make_server
|
|
||||||
server = server_class((host, port), handler_class)
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/socketserver.py", line 457, in __init__
|
|
||||||
self.server_bind()
|
|
||||||
~~~~~~~~~~~~~~~~^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/wsgiref/simple_server.py", line 50, in server_bind
|
|
||||||
HTTPServer.server_bind(self)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~^^^^^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/http/server.py", line 136, in server_bind
|
|
||||||
socketserver.TCPServer.server_bind(self)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/socketserver.py", line 473, in server_bind
|
|
||||||
self.socket.bind(self.server_address)
|
|
||||||
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
OSError: [Errno 48] Address already in use
|
|
||||||
2026-02-21 20:31:25,978 [34mINFO[0m [ExecutionManager] 🧹 Running cleanup
|
|
||||||
2026-02-21 20:31:25,978 [34mINFO[0m [GraphExecutor] [ExecutionManager][on_graph_executor_stop 6094] 🧹 Starting graceful shutdown...
|
|
||||||
2026-02-21 20:31:25,978 [34mINFO[0m [PID-6094|THREAD-77685503|ExecutionManager|RabbitMQ-5b203f2b-8b80-46b1-8e47-481497e68a82] Acquiring connection started...
|
|
||||||
2026-02-21 20:31:25,980 [34mINFO[0m Pika version 1.3.2 connecting to ('::1', 5672, 0, 0)
|
|
||||||
2026-02-21 20:31:25,981 [34mINFO[0m Socket connected: <socket.socket fd=14, family=30, type=1, proto=6, laddr=('::1', 56040, 0, 0), raddr=('::1', 5672, 0, 0)>
|
|
||||||
2026-02-21 20:31:25,982 [34mINFO[0m Streaming transport linked up: (<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1316cd550>, _StreamingProtocolShim: <SelectConnection PROTOCOL transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1316cd550> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>).
|
|
||||||
2026-02-21 20:31:25,991 [34mINFO[0m AMQPConnector - reporting success: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1316cd550> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:25,991 [34mINFO[0m AMQPConnectionWorkflow - reporting success: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1316cd550> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:25,991 [34mINFO[0m Connection workflow succeeded: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1316cd550> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:25,991 [34mINFO[0m Created channel=1
|
|
||||||
2026-02-21 20:31:26,001 [34mINFO[0m [PID-6094|THREAD-77685503|ExecutionManager|RabbitMQ-5b203f2b-8b80-46b1-8e47-481497e68a82] Acquiring connection completed successfully.
|
|
||||||
2026-02-21 20:31:26,001 [34mINFO[0m [GraphExecutor] [ExecutionManager][on_graph_executor_stop 6094] ✅ Exec consumer has been signaled to stop
|
|
||||||
2026-02-21 20:31:26,001 [34mINFO[0m [GraphExecutor] [ExecutionManager][on_graph_executor_stop 6094] ✅ Executor shutdown completed
|
|
||||||
2026-02-21 20:31:26,001 [34mINFO[0m [GraphExecutor] [ExecutionManager][on_graph_executor_stop 6094] ✅ Released execution locks
|
|
||||||
2026-02-21 20:31:26,001 [31mERROR[0m [31m[GraphExecutor] [ExecutionManager][on_graph_executor_stop 6094] [run-consumer] ⚠️ Error disconnecting run client: <class 'RuntimeError'> cannot join thread before it is started [0m
|
|
||||||
2026-02-21 20:31:26,003 [34mINFO[0m [PID-6094|THREAD-77685503|ExecutionManager|RabbitMQ-5b203f2b-8b80-46b1-8e47-481497e68a82] Acquiring connection started...
|
|
||||||
2026-02-21 20:31:26,005 [34mINFO[0m Pika version 1.3.2 connecting to ('::1', 5672, 0, 0)
|
|
||||||
2026-02-21 20:31:26,005 [34mINFO[0m Socket connected: <socket.socket fd=20, family=30, type=1, proto=6, laddr=('::1', 56043, 0, 0), raddr=('::1', 5672, 0, 0)>
|
|
||||||
2026-02-21 20:31:26,006 [34mINFO[0m Streaming transport linked up: (<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1318e4cd0>, _StreamingProtocolShim: <SelectConnection PROTOCOL transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1318e4cd0> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>).
|
|
||||||
2026-02-21 20:31:26,009 [34mINFO[0m Metrics endpoint exposed at /metrics for Scheduler
|
|
||||||
2026-02-21 20:31:26,010 [34mINFO[0m AMQPConnector - reporting success: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1318e4cd0> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:26,010 [34mINFO[0m AMQPConnectionWorkflow - reporting success: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1318e4cd0> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:26,010 [34mINFO[0m Connection workflow succeeded: <SelectConnection OPEN transport=<pika.adapters.utils.io_services_utils._AsyncPlaintextTransport object at 0x1318e4cd0> params=<ConnectionParameters host=localhost port=5672 virtual_host=/ ssl=False>>
|
|
||||||
2026-02-21 20:31:26,011 [34mINFO[0m Created channel=1
|
|
||||||
2026-02-21 20:31:26,015 [34mINFO[0m [PID-6090|THREAD-77685897|Scheduler|FastAPI server-6caca9cc-c4c1-417f-8b83-d96f02472df9] Running FastAPI server started...
|
|
||||||
2026-02-21 20:31:26,016 [34mINFO[0m [Scheduler] Starting RPC server at http://localhost:8003
|
|
||||||
2026-02-21 20:31:26,016 [34mINFO[0m [PID-6094|THREAD-77685503|ExecutionManager|RabbitMQ-5b203f2b-8b80-46b1-8e47-481497e68a82] Acquiring connection completed successfully.
|
|
||||||
2026-02-21 20:31:26,016 [31mERROR[0m [31m[GraphExecutor] [ExecutionManager][on_graph_executor_stop 6094] [cancel-consumer] ⚠️ Error disconnecting run client: <class 'RuntimeError'> cannot join thread before it is started [0m
|
|
||||||
2026-02-21 20:31:26,019 [34mINFO[0m [GraphExecutor] [ExecutionManager][on_graph_executor_stop 6094] ✅ Finished GraphExec cleanup
|
|
||||||
2026-02-21 20:31:26,019 [34mINFO[0m [ExecutionManager] ✅ Cleanup done
|
|
||||||
2026-02-21 20:31:26,019 [34mINFO[0m [ExecutionManager] 🛑 Terminated
|
|
||||||
2026-02-21 20:31:26,188 [34mINFO[0m [PID-6089|THREAD-77685901|DatabaseManager|FastAPI server-7019e67b-30c1-4d08-a0ec-4f0175629d0e] Running FastAPI server started...
|
|
||||||
2026-02-21 20:31:26,189 [34mINFO[0m [DatabaseManager] Starting RPC server at http://localhost:8005
|
|
||||||
2026-02-21 20:31:26,197 [34mINFO[0m [DatabaseManager] ⏳ Connecting to Database...
|
|
||||||
2026-02-21 20:31:26,197 [34mINFO[0m [PID-6089|THREAD-77685902|DatabaseManager|Prisma-64fcde85-3de3-4783-b2c6-789775451cd0] Acquiring connection started...
|
|
||||||
2026-02-21 20:31:26,254 [34mINFO[0m [Scheduler] [APScheduler] Adding job tentatively -- it will be properly scheduled when the scheduler starts
|
|
||||||
2026-02-21 20:31:26,255 [34mINFO[0m [Scheduler] [APScheduler] Adding job tentatively -- it will be properly scheduled when the scheduler starts
|
|
||||||
2026-02-21 20:31:26,255 [34mINFO[0m [Scheduler] [APScheduler] Adding job tentatively -- it will be properly scheduled when the scheduler starts
|
|
||||||
2026-02-21 20:31:26,255 [34mINFO[0m [Scheduler] [APScheduler] Adding job tentatively -- it will be properly scheduled when the scheduler starts
|
|
||||||
2026-02-21 20:31:26,255 [34mINFO[0m [Scheduler] [APScheduler] Adding job tentatively -- it will be properly scheduled when the scheduler starts
|
|
||||||
2026-02-21 20:31:26,255 [34mINFO[0m [Scheduler] [APScheduler] Adding job tentatively -- it will be properly scheduled when the scheduler starts
|
|
||||||
2026-02-21 20:31:26,256 [34mINFO[0m [Scheduler] [APScheduler] Adding job tentatively -- it will be properly scheduled when the scheduler starts
|
|
||||||
2026-02-21 20:31:26,346 [34mINFO[0m [PID-6089|THREAD-77685902|DatabaseManager|Prisma-64fcde85-3de3-4783-b2c6-789775451cd0] Acquiring connection completed successfully.
|
|
||||||
2026-02-21 20:31:26,346 [34mINFO[0m [DatabaseManager] ✅ Ready
|
|
||||||
2026-02-21 20:31:26,347 [31mERROR[0m [31m[Errno 48] error while attempting to bind on address ('::1', 8005, 0, 0): [errno 48] address already in use[0m
|
|
||||||
2026-02-21 20:31:26,349 [34mINFO[0m [DatabaseManager] ⏳ Disconnecting Database...
|
|
||||||
2026-02-21 20:31:26,349 [34mINFO[0m [PID-6089|THREAD-77685902|DatabaseManager|Prisma-2397ec31-7da6-4598-a012-6c48f17ea97f] Releasing connection started...
|
|
||||||
2026-02-21 20:31:26,350 [34mINFO[0m [PID-6089|THREAD-77685902|DatabaseManager|Prisma-2397ec31-7da6-4598-a012-6c48f17ea97f] Releasing connection completed successfully.
|
|
||||||
2026-02-21 20:31:26,351 [34mINFO[0m [DatabaseManager] ✅ FastAPI has finished
|
|
||||||
2026-02-21 20:31:26,351 [34mINFO[0m [DatabaseManager] 🛑 Shared event loop stopped
|
|
||||||
2026-02-21 20:31:26,351 [34mINFO[0m [DatabaseManager] 🧹 Running cleanup
|
|
||||||
Process DatabaseManager:
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/process.py", line 313, in _bootstrap
|
|
||||||
self.run()
|
|
||||||
~~~~~~~~^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/process.py", line 108, in run
|
|
||||||
self._target(*self._args, **self._kwargs)
|
|
||||||
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/backend/util/process.py", line 83, in execute_run_command
|
|
||||||
self.cleanup()
|
|
||||||
~~~~~~~~~~~~^^
|
|
||||||
File "/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/backend/util/service.py", line 153, in cleanup
|
|
||||||
self.shared_event_loop.call_soon_threadsafe(self.shared_event_loop.stop)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 873, in call_soon_threadsafe
|
|
||||||
self._check_closed()
|
|
||||||
~~~~~~~~~~~~~~~~~~^^
|
|
||||||
File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 551, in _check_closed
|
|
||||||
raise RuntimeError('Event loop is closed')
|
|
||||||
RuntimeError: Event loop is closed
|
|
||||||
2026-02-21 20:31:26,382 [34mINFO[0m [Scheduler] [APScheduler] Added job "process_weekly_summary" to job store "weekly_notifications"
|
|
||||||
2026-02-21 20:31:26,390 [34mINFO[0m [Scheduler] [APScheduler] Added job "report_late_executions" to job store "execution"
|
|
||||||
2026-02-21 20:31:26,392 [34mINFO[0m [Scheduler] [APScheduler] Added job "report_block_error_rates" to job store "execution"
|
|
||||||
2026-02-21 20:31:26,395 [34mINFO[0m [Scheduler] [APScheduler] Added job "cleanup_expired_files" to job store "execution"
|
|
||||||
2026-02-21 20:31:26,397 [34mINFO[0m [Scheduler] [APScheduler] Added job "cleanup_oauth_tokens" to job store "execution"
|
|
||||||
2026-02-21 20:31:26,399 [34mINFO[0m [Scheduler] [APScheduler] Added job "execution_accuracy_alerts" to job store "execution"
|
|
||||||
2026-02-21 20:31:26,401 [34mINFO[0m [Scheduler] [APScheduler] Added job "ensure_embeddings_coverage" to job store "execution"
|
|
||||||
2026-02-21 20:31:26,401 [34mINFO[0m [Scheduler] [APScheduler] Scheduler started
|
|
||||||
2026-02-21 20:31:26,402 [34mINFO[0m [Scheduler] Running embedding backfill on startup...
|
|
||||||
2026-02-21 20:31:26,440 [33mWARNING[0m [33mProvider LINEAR implements OAuth but the required env vars LINEAR_CLIENT_ID and LINEAR_CLIENT_SECRET are not both set[0m
|
|
||||||
2026-02-21 20:31:26,468 [34mINFO[0m [PID-6090|THREAD-77685499|Scheduler|AppService client-24942e64-d380-4d36-a245-5c41172e5293] Creating service client started...
|
|
||||||
2026-02-21 20:31:26,468 [34mINFO[0m [PID-6090|THREAD-77685499|Scheduler|AppService client-24942e64-d380-4d36-a245-5c41172e5293] Creating service client completed successfully.
|
|
||||||
2026-02-21 20:31:26,485 [33mWARNING[0m [33mAuthentication error: Langfuse client initialized without public_key. Client will be disabled. Provide a public_key parameter or set LANGFUSE_PUBLIC_KEY environment variable. [0m
|
|
||||||
2026-02-21 20:31:26,652 [34mINFO[0m Metrics endpoint exposed at /metrics for external-api
|
|
||||||
2026-02-21 20:31:26,655 [34mINFO[0m Metrics endpoint exposed at /metrics for rest-api
|
|
||||||
2026-02-21 20:31:26,735 [34mINFO[0m [AgentServer] Starting...
|
|
||||||
2026-02-21 20:31:26,745 [34mINFO[0m Started server process [6093]
|
|
||||||
2026-02-21 20:31:26,745 [34mINFO[0m Waiting for application startup.
|
|
||||||
2026-02-21 20:31:26,746 [33mWARNING[0m [33m⚠️ JWT_SIGN_ALGORITHM is set to 'HS256', a symmetric shared-key signature algorithm. We highly recommend using an asymmetric algorithm such as ES256, because when leaked, a shared secret would allow anyone to forge valid tokens and impersonate users. More info: https://supabase.com/docs/guides/auth/signing-keys#choosing-the-right-signing-algorithm[0m
|
|
||||||
2026-02-21 20:31:26,747 [34mINFO[0m [PID-6093|THREAD-77685502|AgentServer|Prisma-9d930243-0262-4697-b4af-e0bcbec281c4] Acquiring connection started...
|
|
||||||
2026-02-21 20:31:26,812 [34mINFO[0m [PID-6093|THREAD-77685502|AgentServer|Prisma-9d930243-0262-4697-b4af-e0bcbec281c4] Acquiring connection completed successfully.
|
|
||||||
2026-02-21 20:31:26,825 [34mINFO[0m Thread pool size set to 60 for sync endpoint/dependency performance
|
|
||||||
2026-02-21 20:31:26,825 [34mINFO[0m Successfully patched IntegrationCredentialsStore.get_all_creds
|
|
||||||
2026-02-21 20:31:26,825 [34mINFO[0m Syncing provider costs to blocks...
|
|
||||||
2026-02-21 20:31:27,576 [33mWARNING[0m [33mProvider WORDPRESS implements OAuth but the required env vars WORDPRESS_CLIENT_ID and WORDPRESS_CLIENT_SECRET are not both set[0m
|
|
||||||
2026-02-21 20:31:27,631 [34mINFO[0m Registered 1 custom costs for block FirecrawlExtractBlock
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/backend/blocks/exa/helpers.py:56: UserWarning: Field name "schema" in "SummarySettings" shadows an attribute in parent "BaseModel"
|
|
||||||
class SummarySettings(BaseModel):
|
|
||||||
2026-02-21 20:31:27,954 [33mWARNING[0m [33mProvider AIRTABLE implements OAuth but the required env vars AIRTABLE_CLIENT_ID and AIRTABLE_CLIENT_SECRET are not both set[0m
|
|
||||||
2026-02-21 20:31:29,238 [34mINFO[0m Successfully patched IntegrationCredentialsStore.get_all_creds
|
|
||||||
2026-02-21 20:31:29,397 [33mWARNING[0m [33mBlock WordPressCreatePostBlock credential input 'credentials' provider 'wordpress' has no authentication methods configured - Disabling[0m
|
|
||||||
2026-02-21 20:31:29,397 [33mWARNING[0m [33mBlock WordPressGetAllPostsBlock credential input 'credentials' provider 'wordpress' has no authentication methods configured - Disabling[0m
|
|
||||||
2026-02-21 20:31:29,465 [34mINFO[0m Synced 82 costs to 82 blocks
|
|
||||||
2026-02-21 20:31:29,466 [33mWARNING[0m [33mExecuting <Task pending name='Task-2' coro=<LifespanOn.main() running at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/lifespan/on.py:86> created at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/lifespan/on.py:51> took 2.654 seconds[0m
|
|
||||||
2026-02-21 20:31:29,511 [34mINFO[0m [Scheduler] All content has embeddings, skipping backfill
|
|
||||||
2026-02-21 20:31:29,512 [34mINFO[0m [Scheduler] Running cleanup for orphaned embeddings (blocks/docs)...
|
|
||||||
2026-02-21 20:31:29,542 [34mINFO[0m [Scheduler] Cleanup completed: no orphaned embeddings found
|
|
||||||
2026-02-21 20:31:29,542 [34mINFO[0m [Scheduler] Startup embedding backfill complete: {'backfill': {'processed': 0, 'success': 0, 'failed': 0}, 'cleanup': {'deleted': 0}}
|
|
||||||
2026-02-21 20:31:29,553 [34mINFO[0m Started server process [6090]
|
|
||||||
2026-02-21 20:31:29,553 [34mINFO[0m Waiting for application startup.
|
|
||||||
2026-02-21 20:31:29,554 [34mINFO[0m Application startup complete.
|
|
||||||
2026-02-21 20:31:29,555 [34mINFO[0m Uvicorn running on http://localhost:8003 (Press CTRL+C to quit)
|
|
||||||
2026-02-21 20:31:31,074 [34mINFO[0m Migrating integration credentials for 0 users
|
|
||||||
2026-02-21 20:31:31,087 [34mINFO[0m Fixing LLM credential inputs on 0 nodes
|
|
||||||
2026-02-21 20:31:31,087 [34mINFO[0m Migrating LLM models
|
|
||||||
2026-02-21 20:31:31,107 [34mINFO[0m Migrated 0 node triggers to triggered presets
|
|
||||||
2026-02-21 20:31:31,107 [34mINFO[0m [PID-6093|THREAD-77685502|AgentServer|AsyncRedis-f8b888fc-8b03-4807-adfd-c93710c11c85] Acquiring connection started...
|
|
||||||
2026-02-21 20:31:31,114 [34mINFO[0m [PID-6093|THREAD-77685502|AgentServer|AsyncRedis-f8b888fc-8b03-4807-adfd-c93710c11c85] Acquiring connection completed successfully.
|
|
||||||
2026-02-21 20:31:31,115 [34mINFO[0m Created consumer group 'chat_consumers' on stream 'chat:completions'
|
|
||||||
2026-02-21 20:31:31,115 [34mINFO[0m Chat completion consumer started (consumer: consumer-2f92959a)
|
|
||||||
2026-02-21 20:31:31,116 [34mINFO[0m Application startup complete.
|
|
||||||
2026-02-21 20:31:31,117 [34mINFO[0m Uvicorn running on http://0.0.0.0:8006 (Press CTRL+C to quit)
|
|
||||||
2026-02-21 20:31:45,616 [34mINFO[0m 127.0.0.1:56174 - "GET /api/health HTTP/1.1" 404
|
|
||||||
2026-02-21 20:32:07,632 [34mINFO[0m 127.0.0.1:56317 - "GET /openapi.json HTTP/1.1" 200
|
|
||||||
2026-02-21 20:32:07,635 [33mWARNING[0m [33mExecuting <Task finished name='Task-7' coro=<RequestResponseCycle.run_asgi() done, defined at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:414> result=None created at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:295> took 0.346 seconds[0m
|
|
||||||
2026-02-21 20:32:41,502 [34mINFO[0m 127.0.0.1:56681 - "POST /api/v2/chat/sessions HTTP/1.1" 404
|
|
||||||
2026-02-21 20:32:50,005 [34mINFO[0m 127.0.0.1:56736 - "GET /api/docs HTTP/1.1" 404
|
|
||||||
2026-02-21 20:33:10,267 [34mINFO[0m 127.0.0.1:56898 - "GET /openapi.json HTTP/1.1" 200
|
|
||||||
2026-02-21 20:33:28,399 [34mINFO[0m 127.0.0.1:56993 - "POST /api/chat/sessions HTTP/1.1" 401
|
|
||||||
2026-02-21 20:34:20,913 [34mINFO[0m 127.0.0.1:57313 - "GET /openapi.json HTTP/1.1" 200
|
|
||||||
2026-02-21 20:36:26,326 [34mINFO[0m Running job "report_late_executions (trigger: interval[0:05:00], next run at: 2026-02-21 13:36:26 UTC)" (scheduled at 2026-02-21 13:36:26.255260+00:00)
|
|
||||||
2026-02-21 20:36:26,333 [34mINFO[0m [PID-6090|THREAD-77695300|Scheduler|AppService client-24942e64-d380-4d36-a245-5c41172e5293] Creating service client started...
|
|
||||||
2026-02-21 20:36:26,336 [34mINFO[0m [PID-6090|THREAD-77695300|Scheduler|AppService client-24942e64-d380-4d36-a245-5c41172e5293] Creating service client completed successfully.
|
|
||||||
2026-02-21 20:36:26,336 [34mINFO[0m [PID-6090|THREAD-77695300|Scheduler|AppService client-24942e64-d380-4d36-a245-5c41172e5293] Creating service client started...
|
|
||||||
2026-02-21 20:36:26,340 [34mINFO[0m [PID-6090|THREAD-77695300|Scheduler|AppService client-24942e64-d380-4d36-a245-5c41172e5293] Creating service client completed successfully.
|
|
||||||
2026-02-21 20:36:26,439 [33mWARNING[0m [33mService communication: Retry attempt 1 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:36:27,802 [33mWARNING[0m [33mService communication: Retry attempt 2 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:36:30,362 [33mWARNING[0m [33mService communication: Retry attempt 3 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:36:34,885 [33mWARNING[0m [33mService communication: Retry attempt 4 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:36:43,438 [33mWARNING[0m [33mService communication: Retry attempt 5 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:36:59,905 [33mWARNING[0m [33mService communication: Retry attempt 6 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:37:12,581 [33mWARNING[0m [33mExecuting <Task pending name='Task-13' coro=<RequestResponseCycle.run_asgi() running at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:416> cb=[set.discard()] created at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:295> took 0.109 seconds[0m
|
|
||||||
2026-02-21 20:37:12,767 [34mINFO[0m 127.0.0.1:58472 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:12,886 [34mINFO[0m 127.0.0.1:58469 - "GET /api/chat/sessions?limit=50 HTTP/1.1" 200
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/expressions/parser.py:72: PyparsingDeprecationWarning: 'enablePackrat' deprecated - use 'enable_packrat'
|
|
||||||
ParserElement.enablePackrat()
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/expressions/parser.py:85: PyparsingDeprecationWarning: 'escChar' argument is deprecated, use 'esc_char'
|
|
||||||
quoted_identifier = QuotedString('"', escChar="\\", unquoteResults=True)
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/expressions/parser.py:85: PyparsingDeprecationWarning: 'unquoteResults' argument is deprecated, use 'unquote_results'
|
|
||||||
quoted_identifier = QuotedString('"', escChar="\\", unquoteResults=True)
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:365: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
|
|
||||||
@model_validator(mode="after")
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:494: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
|
|
||||||
@model_validator(mode="after")
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:498: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
|
|
||||||
@model_validator(mode="after")
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:502: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
|
|
||||||
@model_validator(mode="after")
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:506: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
|
|
||||||
@model_validator(mode="after")
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:538: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
|
|
||||||
@model_validator(mode="after")
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:542: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
|
|
||||||
@model_validator(mode="after")
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:546: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
|
|
||||||
@model_validator(mode="after")
|
|
||||||
/Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:550: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
|
|
||||||
@model_validator(mode="after")
|
|
||||||
2026-02-21 20:37:14,074 [34mINFO[0m 127.0.0.1:58470 - "GET /api/executions HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:14,081 [33mWARNING[0m [33mExecuting <Task finished name='Task-14' coro=<RequestResponseCycle.run_asgi() done, defined at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:414> result=None created at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:295> took 1.169 seconds[0m
|
|
||||||
2026-02-21 20:37:15,102 [33mWARNING[0m [33mExecuting <Task pending name='Task-1' coro=<Server.serve() running at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/server.py:71> wait_for=<Future pending cb=[Task.task_wakeup()] created at /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/tasks.py:713> cb=[run_until_complete.<locals>.done_cb()] created at /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py:100> took 0.224 seconds[0m
|
|
||||||
2026-02-21 20:37:17,085 [34mINFO[0m 127.0.0.1:58530 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:20,772 [33mWARNING[0m [33mExecuting <Task pending name='Task-1' coro=<Server.serve() running at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/server.py:71> wait_for=<Future pending cb=[Task.task_wakeup()] created at /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/tasks.py:713> cb=[run_until_complete.<locals>.done_cb()] created at /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py:100> took 0.261 seconds[0m
|
|
||||||
2026-02-21 20:37:21,276 [34mINFO[0m 127.0.0.1:58568 - "GET /api/integrations/providers/system HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:21,309 [33mWARNING[0m [33mExecuting <Task finished name='Task-23' coro=<RequestResponseCycle.run_asgi() done, defined at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:414> result=None created at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:295> took 0.158 seconds[0m
|
|
||||||
2026-02-21 20:37:21,329 [34mINFO[0m 127.0.0.1:58570 - "GET /api/integrations/providers HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:21,421 [33mWARNING[0m [33mExecuting <Task finished name='Task-24' coro=<RequestResponseCycle.run_asgi() done, defined at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:414> result=None created at /Users/majdyz/Code/AutoGPT/autogpt_platform/backend/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py:295> took 0.110 seconds[0m
|
|
||||||
2026-02-21 20:37:22,406 [34mINFO[0m 127.0.0.1:58590 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:22,430 [34mINFO[0m 127.0.0.1:58588 - "GET /api/onboarding HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:22,453 [34mINFO[0m 127.0.0.1:58570 - "GET /api/executions HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:22,476 [34mINFO[0m Loaded session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from DB: has_messages=True, message_count=11, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool']
|
|
||||||
2026-02-21 20:37:22,485 [34mINFO[0m Cached session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from database
|
|
||||||
2026-02-21 20:37:22,510 [34mINFO[0m 127.0.0.1:58568 - "GET /api/library/agents?page=1&page_size=100 HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:22,515 [34mINFO[0m [GET_SESSION] session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, active_task=False, msg_count=11, last_role=tool
|
|
||||||
2026-02-21 20:37:22,524 [34mINFO[0m 127.0.0.1:58599 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:22,535 [34mINFO[0m 127.0.0.1:58607 - "GET /api/chat/sessions?limit=50 HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:22,608 [34mINFO[0m 127.0.0.1:58568 - "GET /api/integrations/credentials HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:23,531 [34mINFO[0m 127.0.0.1:58568 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:25,612 [34mINFO[0m 127.0.0.1:58568 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:29,708 [34mINFO[0m 127.0.0.1:58671 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:29,975 [33mWARNING[0m [33mService communication: Retry attempt 7 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:37:34,125 [34mINFO[0m [TIMING] stream_chat_post STARTED, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, user=68383665-d3d9-41f3-b10c-fca0dc6080ed, message_len=36
|
|
||||||
2026-02-21 20:37:34,134 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=11, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool']
|
|
||||||
2026-02-21 20:37:34,135 [34mINFO[0m [TIMING] session validated in 10.6ms
|
|
||||||
2026-02-21 20:37:34,136 [34mINFO[0m [STREAM] Saving user message to session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f
|
|
||||||
2026-02-21 20:37:34,138 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=11, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool']
|
|
||||||
2026-02-21 20:37:34,168 [34mINFO[0m Saving 1 new messages to DB for session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f: roles=['user'], start_sequence=11
|
|
||||||
2026-02-21 20:37:34,201 [34mINFO[0m [STREAM] User message saved for session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f
|
|
||||||
2026-02-21 20:37:34,202 [34mINFO[0m [TIMING] create_task STARTED, task=bba63941-8048-4f39-9329-8568e5ebe9cd, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, user=68383665-d3d9-41f3-b10c-fca0dc6080ed
|
|
||||||
2026-02-21 20:37:34,202 [34mINFO[0m [TIMING] get_redis_async took 0.0ms
|
|
||||||
2026-02-21 20:37:34,205 [34mINFO[0m [TIMING] redis.hset took 2.9ms
|
|
||||||
2026-02-21 20:37:34,208 [34mINFO[0m [TIMING] create_task COMPLETED in 6.1ms; task=bba63941-8048-4f39-9329-8568e5ebe9cd, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f
|
|
||||||
2026-02-21 20:37:34,208 [34mINFO[0m [TIMING] create_task completed in 6.8ms
|
|
||||||
2026-02-21 20:37:34,210 [34mINFO[0m [PID-6093|THREAD-77685502|AgentServer|AsyncRabbitMQ-bbe1cabd-35fe-4944-89d1-fddd09c93923] Acquiring async connection started...
|
|
||||||
2026-02-21 20:37:34,296 [34mINFO[0m [PID-6093|THREAD-77685502|AgentServer|AsyncRabbitMQ-bbe1cabd-35fe-4944-89d1-fddd09c93923] Acquiring async connection completed successfully.
|
|
||||||
2026-02-21 20:37:34,305 [34mINFO[0m [TIMING] Task enqueued to RabbitMQ, setup=180.6ms
|
|
||||||
2026-02-21 20:37:34,307 [34mINFO[0m [TIMING] event_generator STARTED, task=bba63941-8048-4f39-9329-8568e5ebe9cd, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, user=68383665-d3d9-41f3-b10c-fca0dc6080ed
|
|
||||||
2026-02-21 20:37:34,307 [34mINFO[0m [TIMING] subscribe_to_task STARTED, task=bba63941-8048-4f39-9329-8568e5ebe9cd, user=68383665-d3d9-41f3-b10c-fca0dc6080ed, last_msg=0-0
|
|
||||||
2026-02-21 20:37:34,309 [34mINFO[0m [TIMING] Redis hgetall took 2.1ms
|
|
||||||
2026-02-21 20:37:34,353 [34mINFO[0m [PID-6048|THREAD-77685506|CoPilotExecutor|Redis-943506d1-86e7-48a7-871b-9977fb0ace47] Acquiring connection started...
|
|
||||||
2026-02-21 20:37:34,435 [34mINFO[0m [PID-6048|THREAD-77685506|CoPilotExecutor|Redis-943506d1-86e7-48a7-871b-9977fb0ace47] Acquiring connection completed successfully.
|
|
||||||
2026-02-21 20:37:34,442 [34mINFO[0m [CoPilotExecutor] Acquired cluster lock for bba63941-8048-4f39-9329-8568e5ebe9cd, executor_id=fb7d76b3-8dc3-40a4-947e-a93bfad207da
|
|
||||||
2026-02-21 20:37:34,535 [34mINFO[0m [CoPilotExecutor] [CoPilotExecutor] Worker 13455405056 started
|
|
||||||
2026-02-21 20:37:34,536 [34mINFO[0m [CoPilotExecutor|task_id:bba63941-8048-4f39-9329-8568e5ebe9cd|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Starting execution
|
|
||||||
2026-02-21 20:37:35,596 [34mINFO[0m [CoPilotExecutor|task_id:bba63941-8048-4f39-9329-8568e5ebe9cd|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Using SDK service
|
|
||||||
2026-02-21 20:37:35,596 [34mINFO[0m [PID-6048|THREAD-77697399|CoPilotExecutor|AsyncRedis-2e10c980-0364-4c4b-9b2d-8186f23b1735] Acquiring connection started...
|
|
||||||
2026-02-21 20:37:35,600 [34mINFO[0m [PID-6048|THREAD-77697399|CoPilotExecutor|AsyncRedis-2e10c980-0364-4c4b-9b2d-8186f23b1735] Acquiring connection completed successfully.
|
|
||||||
2026-02-21 20:37:35,601 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=12, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user']
|
|
||||||
2026-02-21 20:37:35,601 [34mINFO[0m [PID-6048|THREAD-77697399|CoPilotExecutor|AppService client-34797c8f-0201-4f99-bf73-3f3fb4697e6d] Creating service client started...
|
|
||||||
2026-02-21 20:37:35,601 [34mINFO[0m [PID-6048|THREAD-77697399|CoPilotExecutor|AppService client-34797c8f-0201-4f99-bf73-3f3fb4697e6d] Creating service client completed successfully.
|
|
||||||
2026-02-21 20:37:35,657 [33mWARNING[0m [33mService communication: Retry attempt 1 for '_call_method_async': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_chat_session_message_count'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:37:36,713 [33mWARNING[0m [33mService communication: Retry attempt 2 for '_call_method_async': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_chat_session_message_count'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:37:39,646 [33mWARNING[0m [33mService communication: Retry attempt 3 for '_call_method_async': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_chat_session_message_count'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:37:43,415 [34mINFO[0m 127.0.0.1:58782 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:44,423 [33mWARNING[0m [33mService communication: Retry attempt 4 for '_call_method_async': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_chat_session_message_count'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:37:44,486 [34mINFO[0m 127.0.0.1:58782 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:45,048 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=12, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user']
|
|
||||||
2026-02-21 20:37:45,053 [34mINFO[0m [TASK_LOOKUP] Found running task bba63941... for session 322af5c3...
|
|
||||||
2026-02-21 20:37:45,063 [34mINFO[0m [CoPilotExecutor] Received cancel for bba63941-8048-4f39-9329-8568e5ebe9cd
|
|
||||||
2026-02-21 20:37:45,064 [34mINFO[0m [CANCEL] Published cancel for task ...e5ebe9cd session ...f0aa0c9f
|
|
||||||
2026-02-21 20:37:45,113 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=12, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user']
|
|
||||||
2026-02-21 20:37:45,120 [34mINFO[0m [TASK_LOOKUP] Found running task bba63941... for session 322af5c3...
|
|
||||||
2026-02-21 20:37:45,121 [34mINFO[0m [GET_SESSION] session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, active_task=True, msg_count=12, last_role=user
|
|
||||||
2026-02-21 20:37:45,123 [34mINFO[0m 127.0.0.1:58802 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:45,306 [34mINFO[0m [TASK_LOOKUP] Found running task bba63941... for session 322af5c3...
|
|
||||||
2026-02-21 20:37:45,307 [34mINFO[0m [TIMING] subscribe_to_task STARTED, task=bba63941-8048-4f39-9329-8568e5ebe9cd, user=68383665-d3d9-41f3-b10c-fca0dc6080ed, last_msg=0-0
|
|
||||||
2026-02-21 20:37:45,309 [34mINFO[0m [TIMING] Redis hgetall took 1.5ms
|
|
||||||
2026-02-21 20:37:45,604 [34mINFO[0m [CoPilotExecutor|task_id:bba63941-8048-4f39-9329-8568e5ebe9cd|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Cancellation requested
|
|
||||||
2026-02-21 20:37:45,604 [34mINFO[0m [CoPilotExecutor|task_id:bba63941-8048-4f39-9329-8568e5ebe9cd|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Execution completed in 11.07s
|
|
||||||
2026-02-21 20:37:45,604 [34mINFO[0m [CoPilotExecutor] Run completed for bba63941-8048-4f39-9329-8568e5ebe9cd
|
|
||||||
2026-02-21 20:37:45,604 [34mINFO[0m [CoPilotExecutor|task_id:bba63941-8048-4f39-9329-8568e5ebe9cd|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Task cancelled
|
|
||||||
2026-02-21 20:37:45,605 [34mINFO[0m [CoPilotExecutor] Releasing cluster lock for bba63941-8048-4f39-9329-8568e5ebe9cd
|
|
||||||
2026-02-21 20:37:45,609 [34mINFO[0m [CoPilotExecutor] Cleaned up completed task bba63941-8048-4f39-9329-8568e5ebe9cd
|
|
||||||
2026-02-21 20:37:45,610 [34mINFO[0m [TIMING] Redis xread (replay) took 301.1ms, status=running
|
|
||||||
2026-02-21 20:37:45,610 [34mINFO[0m [TIMING] publish_chunk StreamFinish in 1.8ms (xadd=1.3ms)
|
|
||||||
2026-02-21 20:37:45,612 [34mINFO[0m [TIMING] Replayed 1 messages, last_id=1771681065606-0
|
|
||||||
2026-02-21 20:37:45,612 [34mINFO[0m [TIMING] Task still running, starting _stream_listener
|
|
||||||
2026-02-21 20:37:45,613 [34mINFO[0m [TIMING] subscribe_to_task COMPLETED in 305.8ms; task=bba63941-8048-4f39-9329-8568e5ebe9cd, n_messages_replayed=1
|
|
||||||
2026-02-21 20:37:45,614 [34mINFO[0m [TIMING] _stream_listener STARTED, task=bba63941-8048-4f39-9329-8568e5ebe9cd, last_id=1771681065606-0
|
|
||||||
2026-02-21 20:37:45,614 [34mINFO[0m Resume stream chunk
|
|
||||||
2026-02-21 20:37:45,615 [34mINFO[0m 127.0.0.1:58802 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f/stream HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:45,615 [34mINFO[0m [TIMING] Redis xread (replay) took 11305.8ms, status=running
|
|
||||||
2026-02-21 20:37:45,616 [34mINFO[0m [TIMING] Replayed 1 messages, last_id=1771681065606-0
|
|
||||||
2026-02-21 20:37:45,616 [34mINFO[0m [TIMING] Task still running, starting _stream_listener
|
|
||||||
2026-02-21 20:37:45,616 [34mINFO[0m [TIMING] subscribe_to_task COMPLETED in 11308.9ms; task=bba63941-8048-4f39-9329-8568e5ebe9cd, n_messages_replayed=1
|
|
||||||
2026-02-21 20:37:45,616 [34mINFO[0m [TIMING] Starting to read from subscriber_queue
|
|
||||||
2026-02-21 20:37:45,616 [34mINFO[0m [TIMING] FIRST CHUNK from queue at 11.31s, type=StreamFinish
|
|
||||||
2026-02-21 20:37:45,616 [34mINFO[0m 127.0.0.1:58710 - "POST /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f/stream HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:45,617 [34mINFO[0m [TIMING] StreamFinish received in 11.31s; n_chunks=1
|
|
||||||
2026-02-21 20:37:45,617 [34mINFO[0m [TIMING] _stream_listener CANCELLED after 3.5ms, delivered=0
|
|
||||||
2026-02-21 20:37:45,617 [34mINFO[0m [TIMING] _stream_listener FINISHED in 0.0s; task=bba63941-8048-4f39-9329-8568e5ebe9cd, delivered=0, xread_count=1
|
|
||||||
2026-02-21 20:37:45,618 [34mINFO[0m Resume stream completed
|
|
||||||
2026-02-21 20:37:45,618 [34mINFO[0m [TIMING] event_generator FINISHED in 11.31s; task=bba63941-8048-4f39-9329-8568e5ebe9cd, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, n_chunks=1
|
|
||||||
2026-02-21 20:37:45,691 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=12, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user']
|
|
||||||
2026-02-21 20:37:45,694 [34mINFO[0m [GET_SESSION] session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, active_task=False, msg_count=12, last_role=user
|
|
||||||
2026-02-21 20:37:45,695 [34mINFO[0m 127.0.0.1:58710 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:45,710 [34mINFO[0m 127.0.0.1:58802 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f/stream HTTP/1.1" 204
|
|
||||||
2026-02-21 20:37:45,771 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=12, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user']
|
|
||||||
2026-02-21 20:37:45,775 [34mINFO[0m [GET_SESSION] session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, active_task=False, msg_count=12, last_role=user
|
|
||||||
2026-02-21 20:37:45,775 [34mINFO[0m 127.0.0.1:58710 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:46,075 [34mINFO[0m [CANCEL] Task ...e5ebe9cd confirmed stopped (status=failed) after 1.0s
|
|
||||||
2026-02-21 20:37:46,076 [34mINFO[0m 127.0.0.1:58782 - "POST /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f/cancel HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:46,573 [34mINFO[0m 127.0.0.1:58710 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:50,090 [34mINFO[0m 127.0.0.1:58710 - "GET /api/integrations/providers/system HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:50,103 [34mINFO[0m 127.0.0.1:58842 - "GET /api/integrations/providers HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:50,681 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=12, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user']
|
|
||||||
2026-02-21 20:37:50,686 [34mINFO[0m 127.0.0.1:58710 - "GET /api/library/agents?page=1&page_size=100 HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:50,692 [34mINFO[0m 127.0.0.1:58850 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:50,702 [34mINFO[0m 127.0.0.1:58842 - "GET /api/integrations/credentials HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:50,710 [34mINFO[0m [GET_SESSION] session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, active_task=False, msg_count=12, last_role=user
|
|
||||||
2026-02-21 20:37:50,711 [34mINFO[0m 127.0.0.1:58862 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:50,714 [34mINFO[0m 127.0.0.1:58852 - "GET /api/onboarding HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:50,720 [34mINFO[0m 127.0.0.1:58854 - "GET /api/executions HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:50,795 [34mINFO[0m 127.0.0.1:58710 - "GET /api/chat/sessions?limit=50 HTTP/1.1" 200
|
|
||||||
2026-02-21 20:37:51,955 [34mINFO[0m 127.0.0.1:58710 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:54,064 [34mINFO[0m 127.0.0.1:58710 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:54,157 [34mINFO[0m [TIMING] stream_chat_post STARTED, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, user=68383665-d3d9-41f3-b10c-fca0dc6080ed, message_len=5
|
|
||||||
2026-02-21 20:37:54,169 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=12, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user']
|
|
||||||
2026-02-21 20:37:54,170 [34mINFO[0m [TIMING] session validated in 13.0ms
|
|
||||||
2026-02-21 20:37:54,170 [34mINFO[0m [STREAM] Saving user message to session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f
|
|
||||||
2026-02-21 20:37:54,172 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=12, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user']
|
|
||||||
2026-02-21 20:37:54,212 [34mINFO[0m Saving 1 new messages to DB for session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f: roles=['user'], start_sequence=12
|
|
||||||
2026-02-21 20:37:54,238 [34mINFO[0m [STREAM] User message saved for session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f
|
|
||||||
2026-02-21 20:37:54,238 [34mINFO[0m [TIMING] create_task STARTED, task=6360d249-c803-47d3-8a08-d77275e4b2d8, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, user=68383665-d3d9-41f3-b10c-fca0dc6080ed
|
|
||||||
2026-02-21 20:37:54,238 [34mINFO[0m [TIMING] get_redis_async took 0.0ms
|
|
||||||
2026-02-21 20:37:54,242 [34mINFO[0m [TIMING] redis.hset took 3.1ms
|
|
||||||
2026-02-21 20:37:54,250 [34mINFO[0m [TIMING] create_task COMPLETED in 11.6ms; task=6360d249-c803-47d3-8a08-d77275e4b2d8, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f
|
|
||||||
2026-02-21 20:37:54,251 [34mINFO[0m [TIMING] create_task completed in 12.9ms
|
|
||||||
2026-02-21 20:37:54,261 [34mINFO[0m [TIMING] Task enqueued to RabbitMQ, setup=103.8ms
|
|
||||||
2026-02-21 20:37:54,262 [34mINFO[0m [TIMING] event_generator STARTED, task=6360d249-c803-47d3-8a08-d77275e4b2d8, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, user=68383665-d3d9-41f3-b10c-fca0dc6080ed
|
|
||||||
2026-02-21 20:37:54,263 [34mINFO[0m [TIMING] subscribe_to_task STARTED, task=6360d249-c803-47d3-8a08-d77275e4b2d8, user=68383665-d3d9-41f3-b10c-fca0dc6080ed, last_msg=0-0
|
|
||||||
2026-02-21 20:37:54,264 [34mINFO[0m [TIMING] Redis hgetall took 1.7ms
|
|
||||||
2026-02-21 20:37:54,265 [34mINFO[0m [CoPilotExecutor] Acquired cluster lock for 6360d249-c803-47d3-8a08-d77275e4b2d8, executor_id=fb7d76b3-8dc3-40a4-947e-a93bfad207da
|
|
||||||
2026-02-21 20:37:54,267 [34mINFO[0m [CoPilotExecutor|task_id:6360d249-c803-47d3-8a08-d77275e4b2d8|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Starting execution
|
|
||||||
2026-02-21 20:37:54,286 [34mINFO[0m [CoPilotExecutor|task_id:6360d249-c803-47d3-8a08-d77275e4b2d8|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Using SDK service
|
|
||||||
2026-02-21 20:37:54,290 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=13, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user', 'user']
|
|
||||||
2026-02-21 20:37:54,357 [33mWARNING[0m [33mService communication: Retry attempt 1 for '_call_method_async': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_chat_session_message_count'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:37:56,312 [33mWARNING[0m [33mService communication: Retry attempt 2 for '_call_method_async': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_chat_session_message_count'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:37:58,224 [34mINFO[0m 127.0.0.1:58917 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:37:58,928 [33mWARNING[0m [33mService communication: Retry attempt 3 for '_call_method_async': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_chat_session_message_count'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:38:00,041 [33mWARNING[0m [33mService communication: Retry attempt 8 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:38:03,701 [33mWARNING[0m [33mService communication: Retry attempt 4 for '_call_method_async': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_chat_session_message_count'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:38:06,882 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=13, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user', 'user']
|
|
||||||
2026-02-21 20:38:06,888 [34mINFO[0m [TASK_LOOKUP] Found running task 6360d249... for session 322af5c3...
|
|
||||||
2026-02-21 20:38:06,898 [34mINFO[0m [CoPilotExecutor] Received cancel for 6360d249-c803-47d3-8a08-d77275e4b2d8
|
|
||||||
2026-02-21 20:38:06,898 [34mINFO[0m [CANCEL] Published cancel for task ...75e4b2d8 session ...f0aa0c9f
|
|
||||||
2026-02-21 20:38:06,919 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=13, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user', 'user']
|
|
||||||
2026-02-21 20:38:06,925 [34mINFO[0m [TASK_LOOKUP] Found running task 6360d249... for session 322af5c3...
|
|
||||||
2026-02-21 20:38:06,926 [34mINFO[0m [GET_SESSION] session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, active_task=True, msg_count=13, last_role=user
|
|
||||||
2026-02-21 20:38:06,927 [34mINFO[0m 127.0.0.1:58976 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f HTTP/1.1" 200
|
|
||||||
2026-02-21 20:38:07,136 [34mINFO[0m [TASK_LOOKUP] Found running task 6360d249... for session 322af5c3...
|
|
||||||
2026-02-21 20:38:07,138 [34mINFO[0m [TIMING] subscribe_to_task STARTED, task=6360d249-c803-47d3-8a08-d77275e4b2d8, user=68383665-d3d9-41f3-b10c-fca0dc6080ed, last_msg=0-0
|
|
||||||
2026-02-21 20:38:07,140 [34mINFO[0m [TIMING] Redis hgetall took 1.3ms
|
|
||||||
2026-02-21 20:38:07,359 [34mINFO[0m [CoPilotExecutor|task_id:6360d249-c803-47d3-8a08-d77275e4b2d8|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Cancellation requested
|
|
||||||
2026-02-21 20:38:07,360 [34mINFO[0m [CoPilotExecutor|task_id:6360d249-c803-47d3-8a08-d77275e4b2d8|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Execution completed in 13.09s
|
|
||||||
2026-02-21 20:38:07,360 [34mINFO[0m [CoPilotExecutor] Run completed for 6360d249-c803-47d3-8a08-d77275e4b2d8
|
|
||||||
2026-02-21 20:38:07,360 [34mINFO[0m [CoPilotExecutor|task_id:6360d249-c803-47d3-8a08-d77275e4b2d8|session_id:322af5c3-70fc-4a06-9443-8c5df0aa0c9f|user_id:68383665-d3d9-41f3-b10c-fca0dc6080ed] Task cancelled
|
|
||||||
2026-02-21 20:38:07,360 [34mINFO[0m [CoPilotExecutor] Releasing cluster lock for 6360d249-c803-47d3-8a08-d77275e4b2d8
|
|
||||||
2026-02-21 20:38:07,362 [34mINFO[0m [CoPilotExecutor] Cleaned up completed task 6360d249-c803-47d3-8a08-d77275e4b2d8
|
|
||||||
2026-02-21 20:38:07,364 [34mINFO[0m [TIMING] Redis xread (replay) took 224.1ms, status=running
|
|
||||||
2026-02-21 20:38:07,364 [34mINFO[0m [TIMING] Replayed 1 messages, last_id=1771681087362-0
|
|
||||||
2026-02-21 20:38:07,365 [34mINFO[0m [TIMING] Task still running, starting _stream_listener
|
|
||||||
2026-02-21 20:38:07,365 [34mINFO[0m [TIMING] publish_chunk StreamFinish in 2.1ms (xadd=1.2ms)
|
|
||||||
2026-02-21 20:38:07,365 [34mINFO[0m [TIMING] subscribe_to_task COMPLETED in 226.8ms; task=6360d249-c803-47d3-8a08-d77275e4b2d8, n_messages_replayed=1
|
|
||||||
2026-02-21 20:38:07,366 [34mINFO[0m [TIMING] _stream_listener STARTED, task=6360d249-c803-47d3-8a08-d77275e4b2d8, last_id=1771681087362-0
|
|
||||||
2026-02-21 20:38:07,366 [34mINFO[0m Resume stream chunk
|
|
||||||
2026-02-21 20:38:07,366 [34mINFO[0m 127.0.0.1:58976 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f/stream HTTP/1.1" 200
|
|
||||||
2026-02-21 20:38:07,367 [34mINFO[0m [TIMING] Redis xread (replay) took 13101.9ms, status=running
|
|
||||||
2026-02-21 20:38:07,367 [34mINFO[0m [TIMING] Replayed 1 messages, last_id=1771681087362-0
|
|
||||||
2026-02-21 20:38:07,367 [34mINFO[0m [TIMING] Task still running, starting _stream_listener
|
|
||||||
2026-02-21 20:38:07,367 [34mINFO[0m [TIMING] subscribe_to_task COMPLETED in 13104.6ms; task=6360d249-c803-47d3-8a08-d77275e4b2d8, n_messages_replayed=1
|
|
||||||
2026-02-21 20:38:07,367 [34mINFO[0m [TIMING] Starting to read from subscriber_queue
|
|
||||||
2026-02-21 20:38:07,368 [34mINFO[0m [TIMING] FIRST CHUNK from queue at 13.11s, type=StreamFinish
|
|
||||||
2026-02-21 20:38:07,368 [34mINFO[0m 127.0.0.1:58710 - "POST /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f/stream HTTP/1.1" 200
|
|
||||||
2026-02-21 20:38:07,368 [34mINFO[0m [TIMING] StreamFinish received in 13.11s; n_chunks=1
|
|
||||||
2026-02-21 20:38:07,368 [34mINFO[0m [TIMING] _stream_listener CANCELLED after 2.7ms, delivered=0
|
|
||||||
2026-02-21 20:38:07,368 [34mINFO[0m [TIMING] _stream_listener FINISHED in 0.0s; task=6360d249-c803-47d3-8a08-d77275e4b2d8, delivered=0, xread_count=1
|
|
||||||
2026-02-21 20:38:07,369 [34mINFO[0m Resume stream completed
|
|
||||||
2026-02-21 20:38:07,369 [34mINFO[0m [TIMING] event_generator FINISHED in 13.11s; task=6360d249-c803-47d3-8a08-d77275e4b2d8, session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, n_chunks=1
|
|
||||||
2026-02-21 20:38:07,408 [34mINFO[0m [CANCEL] Task ...75e4b2d8 confirmed stopped (status=failed) after 0.5s
|
|
||||||
2026-02-21 20:38:07,409 [34mINFO[0m 127.0.0.1:58974 - "POST /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f/cancel HTTP/1.1" 200
|
|
||||||
2026-02-21 20:38:07,447 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=13, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user', 'user']
|
|
||||||
2026-02-21 20:38:07,451 [34mINFO[0m [GET_SESSION] session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, active_task=False, msg_count=13, last_role=user
|
|
||||||
2026-02-21 20:38:07,451 [34mINFO[0m 127.0.0.1:58710 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f HTTP/1.1" 200
|
|
||||||
2026-02-21 20:38:07,468 [34mINFO[0m 127.0.0.1:58710 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f/stream HTTP/1.1" 204
|
|
||||||
2026-02-21 20:38:07,521 [34mINFO[0m Loading session 322af5c3-70fc-4a06-9443-8c5df0aa0c9f from cache: message_count=13, roles=['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool', 'tool', 'assistant', 'tool', 'tool', 'user', 'user']
|
|
||||||
2026-02-21 20:38:07,527 [34mINFO[0m [GET_SESSION] session=322af5c3-70fc-4a06-9443-8c5df0aa0c9f, active_task=False, msg_count=13, last_role=user
|
|
||||||
2026-02-21 20:38:07,528 [34mINFO[0m 127.0.0.1:58710 - "GET /api/chat/sessions/322af5c3-70fc-4a06-9443-8c5df0aa0c9f HTTP/1.1" 200
|
|
||||||
2026-02-21 20:38:18,440 [34mINFO[0m 127.0.0.1:59077 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:38:19,553 [34mINFO[0m 127.0.0.1:59077 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:38:21,643 [34mINFO[0m 127.0.0.1:59077 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:38:30,090 [33mWARNING[0m [33mService communication: Retry attempt 9 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:39:00,123 [33mWARNING[0m [33mService communication: Retry attempt 10 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:39:13,881 [34mINFO[0m 127.0.0.1:59398 - "GET /api/chat/sessions?limit=50 HTTP/1.1" 200
|
|
||||||
2026-02-21 20:39:30,173 [33mWARNING[0m [33mService communication: Retry attempt 11 for '_call_method_sync': HTTPServerError: HTTP 500: Server error '500 Internal Server Error' for url 'http://localhost:8005/get_graph_executions'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500[0m
|
|
||||||
2026-02-21 20:39:35,355 [34mINFO[0m 127.0.0.1:59522 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:39:35,685 [34mINFO[0m 127.0.0.1:59526 - "GET /api/executions HTTP/1.1" 200
|
|
||||||
2026-02-21 20:39:38,916 [34mINFO[0m 127.0.0.1:59522 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
2026-02-21 20:39:40,019 [34mINFO[0m 127.0.0.1:59522 - "GET /api/store/profile HTTP/1.1" 404
|
|
||||||
1
autogpt_platform/backend/.gitignore
vendored
1
autogpt_platform/backend/.gitignore
vendored
@@ -22,4 +22,3 @@ migrations/*/rollback*.sql
|
|||||||
|
|
||||||
# Workspace files
|
# Workspace files
|
||||||
workspaces/
|
workspaces/
|
||||||
sample.logs
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ class ChatConfig(BaseSettings):
|
|||||||
session_ttl: int = Field(default=43200, description="Session TTL in seconds")
|
session_ttl: int = Field(default=43200, description="Session TTL in seconds")
|
||||||
|
|
||||||
# Streaming Configuration
|
# Streaming Configuration
|
||||||
stream_timeout: int = Field(default=300, description="Stream timeout in seconds")
|
|
||||||
max_retries: int = Field(
|
max_retries: int = Field(
|
||||||
default=3,
|
default=3,
|
||||||
description="Max retries for fallback path (SDK handles retries internally)",
|
description="Max retries for fallback path (SDK handles retries internally)",
|
||||||
@@ -39,8 +38,10 @@ class ChatConfig(BaseSettings):
|
|||||||
|
|
||||||
# Long-running operation configuration
|
# Long-running operation configuration
|
||||||
long_running_operation_ttl: int = Field(
|
long_running_operation_ttl: int = Field(
|
||||||
default=600,
|
default=3600,
|
||||||
description="TTL in seconds for long-running operation tracking in Redis (safety net if pod dies)",
|
description="TTL in seconds for long-running operation deduplication lock "
|
||||||
|
"(1 hour, matches stream_ttl). Prevents duplicate operations if pod dies. "
|
||||||
|
"For longer operations, the stream_registry heartbeat keeps them alive.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Stream registry configuration for SSE reconnection
|
# Stream registry configuration for SSE reconnection
|
||||||
@@ -48,6 +49,11 @@ class ChatConfig(BaseSettings):
|
|||||||
default=3600,
|
default=3600,
|
||||||
description="TTL in seconds for stream data in Redis (1 hour)",
|
description="TTL in seconds for stream data in Redis (1 hour)",
|
||||||
)
|
)
|
||||||
|
stream_lock_ttl: int = Field(
|
||||||
|
default=120,
|
||||||
|
description="TTL in seconds for stream lock (2 minutes). Short timeout allows "
|
||||||
|
"reconnection after refresh/crash without long waits.",
|
||||||
|
)
|
||||||
stream_max_length: int = Field(
|
stream_max_length: int = Field(
|
||||||
default=10000,
|
default=10000,
|
||||||
description="Maximum number of messages to store per stream",
|
description="Maximum number of messages to store per stream",
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
|
from prisma.errors import UniqueViolationError
|
||||||
from prisma.models import ChatMessage as PrismaChatMessage
|
from prisma.models import ChatMessage as PrismaChatMessage
|
||||||
from prisma.models import ChatSession as PrismaChatSession
|
from prisma.models import ChatSession as PrismaChatSession
|
||||||
from prisma.types import (
|
from prisma.types import (
|
||||||
@@ -92,10 +93,9 @@ async def add_chat_message(
|
|||||||
function_call: dict[str, Any] | None = None,
|
function_call: dict[str, Any] | None = None,
|
||||||
) -> ChatMessage:
|
) -> ChatMessage:
|
||||||
"""Add a message to a chat session."""
|
"""Add a message to a chat session."""
|
||||||
# Build input dict dynamically rather than using ChatMessageCreateInput directly
|
# Build ChatMessageCreateInput with only non-None values
|
||||||
# because Prisma's TypedDict validation rejects optional fields set to None.
|
# (Prisma TypedDict rejects optional fields set to None)
|
||||||
# We only include fields that have values, then cast at the end.
|
data: ChatMessageCreateInput = {
|
||||||
data: dict[str, Any] = {
|
|
||||||
"Session": {"connect": {"id": session_id}},
|
"Session": {"connect": {"id": session_id}},
|
||||||
"role": role,
|
"role": role,
|
||||||
"sequence": sequence,
|
"sequence": sequence,
|
||||||
@@ -123,7 +123,7 @@ async def add_chat_message(
|
|||||||
where={"id": session_id},
|
where={"id": session_id},
|
||||||
data={"updatedAt": datetime.now(UTC)},
|
data={"updatedAt": datetime.now(UTC)},
|
||||||
),
|
),
|
||||||
PrismaChatMessage.prisma().create(data=cast(ChatMessageCreateInput, data)),
|
PrismaChatMessage.prisma().create(data=data),
|
||||||
)
|
)
|
||||||
return ChatMessage.from_db(message)
|
return ChatMessage.from_db(message)
|
||||||
|
|
||||||
@@ -132,58 +132,93 @@ async def add_chat_messages_batch(
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
messages: list[dict[str, Any]],
|
messages: list[dict[str, Any]],
|
||||||
start_sequence: int,
|
start_sequence: int,
|
||||||
) -> list[ChatMessage]:
|
) -> int:
|
||||||
"""Add multiple messages to a chat session in a batch.
|
"""Add multiple messages to a chat session in a batch.
|
||||||
|
|
||||||
Uses a transaction for atomicity - if any message creation fails,
|
Uses collision detection with retry: tries to create messages starting
|
||||||
the entire batch is rolled back.
|
at start_sequence. If a unique constraint violation occurs (e.g., the
|
||||||
|
streaming loop and long-running callback race), queries the latest
|
||||||
|
sequence and retries with the correct offset. This avoids unnecessary
|
||||||
|
upserts and DB queries in the common case (no collision).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Next sequence number for the next message to be inserted. This equals
|
||||||
|
start_sequence + len(messages) and allows callers to update their
|
||||||
|
counters even when collision detection adjusts start_sequence.
|
||||||
"""
|
"""
|
||||||
if not messages:
|
if not messages:
|
||||||
return []
|
# No messages to add - return current count
|
||||||
|
return start_sequence
|
||||||
|
|
||||||
created_messages = []
|
max_retries = 5
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
# Single timestamp for all messages and session update
|
||||||
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
async with db.transaction() as tx:
|
async with db.transaction() as tx:
|
||||||
for i, msg in enumerate(messages):
|
# Build all message data
|
||||||
# Build input dict dynamically rather than using ChatMessageCreateInput
|
messages_data = []
|
||||||
# directly because Prisma's TypedDict validation rejects optional fields
|
for i, msg in enumerate(messages):
|
||||||
# set to None. We only include fields that have values, then cast.
|
# Build ChatMessageCreateInput with only non-None values
|
||||||
data: dict[str, Any] = {
|
# (Prisma TypedDict rejects optional fields set to None)
|
||||||
"Session": {"connect": {"id": session_id}},
|
# Note: create_many doesn't support nested creates, use sessionId directly
|
||||||
"role": msg["role"],
|
data: ChatMessageCreateInput = {
|
||||||
"sequence": start_sequence + i,
|
"sessionId": session_id,
|
||||||
}
|
"role": msg["role"],
|
||||||
|
"sequence": start_sequence + i,
|
||||||
|
"createdAt": now,
|
||||||
|
}
|
||||||
|
|
||||||
# Add optional string fields
|
# Add optional string fields
|
||||||
if msg.get("content") is not None:
|
if msg.get("content") is not None:
|
||||||
data["content"] = msg["content"]
|
data["content"] = msg["content"]
|
||||||
if msg.get("name") is not None:
|
if msg.get("name") is not None:
|
||||||
data["name"] = msg["name"]
|
data["name"] = msg["name"]
|
||||||
if msg.get("tool_call_id") is not None:
|
if msg.get("tool_call_id") is not None:
|
||||||
data["toolCallId"] = msg["tool_call_id"]
|
data["toolCallId"] = msg["tool_call_id"]
|
||||||
if msg.get("refusal") is not None:
|
if msg.get("refusal") is not None:
|
||||||
data["refusal"] = msg["refusal"]
|
data["refusal"] = msg["refusal"]
|
||||||
|
|
||||||
# Add optional JSON fields only when they have values
|
# Add optional JSON fields only when they have values
|
||||||
if msg.get("tool_calls") is not None:
|
if msg.get("tool_calls") is not None:
|
||||||
data["toolCalls"] = SafeJson(msg["tool_calls"])
|
data["toolCalls"] = SafeJson(msg["tool_calls"])
|
||||||
if msg.get("function_call") is not None:
|
if msg.get("function_call") is not None:
|
||||||
data["functionCall"] = SafeJson(msg["function_call"])
|
data["functionCall"] = SafeJson(msg["function_call"])
|
||||||
|
|
||||||
created = await PrismaChatMessage.prisma(tx).create(
|
messages_data.append(data)
|
||||||
data=cast(ChatMessageCreateInput, data)
|
|
||||||
)
|
|
||||||
created_messages.append(created)
|
|
||||||
|
|
||||||
# Update session's updatedAt timestamp within the same transaction.
|
# Run create_many and session update in parallel within transaction
|
||||||
# Note: Token usage (total_prompt_tokens, total_completion_tokens) is updated
|
# Both use the same timestamp for consistency
|
||||||
# separately via update_chat_session() after streaming completes.
|
await asyncio.gather(
|
||||||
await PrismaChatSession.prisma(tx).update(
|
PrismaChatMessage.prisma(tx).create_many(data=messages_data),
|
||||||
where={"id": session_id},
|
PrismaChatSession.prisma(tx).update(
|
||||||
data={"updatedAt": datetime.now(UTC)},
|
where={"id": session_id},
|
||||||
)
|
data={"updatedAt": now},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
return [ChatMessage.from_db(m) for m in created_messages]
|
# Return next sequence number for counter sync
|
||||||
|
return start_sequence + len(messages)
|
||||||
|
|
||||||
|
except UniqueViolationError:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
# Collision detected - query MAX(sequence)+1 and retry with correct offset
|
||||||
|
logger.info(
|
||||||
|
f"Collision detected for session {session_id} at sequence "
|
||||||
|
f"{start_sequence}, querying DB for latest sequence"
|
||||||
|
)
|
||||||
|
start_sequence = await get_next_sequence(session_id)
|
||||||
|
logger.info(
|
||||||
|
f"Retrying batch insert with start_sequence={start_sequence}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Max retries exceeded - propagate error
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Should never reach here due to raise in exception handler
|
||||||
|
raise RuntimeError(f"Failed to insert messages after {max_retries} attempts")
|
||||||
|
|
||||||
|
|
||||||
async def get_user_chat_sessions(
|
async def get_user_chat_sessions(
|
||||||
@@ -237,10 +272,20 @@ async def delete_chat_session(session_id: str, user_id: str | None = None) -> bo
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def get_chat_session_message_count(session_id: str) -> int:
|
async def get_next_sequence(session_id: str) -> int:
|
||||||
"""Get the number of messages in a chat session."""
|
"""Get the next sequence number for a new message in this session.
|
||||||
count = await PrismaChatMessage.prisma().count(where={"sessionId": session_id})
|
|
||||||
return count
|
Uses MAX(sequence) + 1 for robustness. Returns 0 if no messages exist.
|
||||||
|
More robust than COUNT(*) because it's immune to deleted messages.
|
||||||
|
|
||||||
|
Optimized to select only the sequence column using raw SQL.
|
||||||
|
The unique index on (sessionId, sequence) makes this query fast.
|
||||||
|
"""
|
||||||
|
results = await db.query_raw_with_schema(
|
||||||
|
'SELECT "sequence" FROM {schema_prefix}"ChatMessage" WHERE "sessionId" = $1 ORDER BY "sequence" DESC LIMIT 1',
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
return 0 if not results else results[0]["sequence"] + 1
|
||||||
|
|
||||||
|
|
||||||
async def update_tool_message_content(
|
async def update_tool_message_content(
|
||||||
|
|||||||
@@ -266,7 +266,11 @@ class CoPilotProcessor:
|
|||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
log.info("Task cancelled")
|
log.info("Task cancelled")
|
||||||
await stream_registry.mark_task_completed(entry.task_id, status="failed")
|
await stream_registry.mark_task_completed(
|
||||||
|
entry.task_id,
|
||||||
|
status="failed",
|
||||||
|
error_message="Task was cancelled",
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -434,8 +434,6 @@ async def _get_session_from_db(session_id: str) -> ChatSession | None:
|
|||||||
|
|
||||||
async def upsert_chat_session(
|
async def upsert_chat_session(
|
||||||
session: ChatSession,
|
session: ChatSession,
|
||||||
*,
|
|
||||||
existing_message_count: int | None = None,
|
|
||||||
) -> ChatSession:
|
) -> ChatSession:
|
||||||
"""Update a chat session in both cache and database.
|
"""Update a chat session in both cache and database.
|
||||||
|
|
||||||
@@ -443,12 +441,6 @@ async def upsert_chat_session(
|
|||||||
operations (e.g., background title update and main stream handler)
|
operations (e.g., background title update and main stream handler)
|
||||||
attempt to upsert the same session simultaneously.
|
attempt to upsert the same session simultaneously.
|
||||||
|
|
||||||
Args:
|
|
||||||
existing_message_count: If provided, skip the DB query to count
|
|
||||||
existing messages. The caller is responsible for tracking this
|
|
||||||
accurately. Useful for incremental saves in a streaming loop
|
|
||||||
where the caller already knows how many messages are persisted.
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
DatabaseError: If the database write fails. The cache is still updated
|
DatabaseError: If the database write fails. The cache is still updated
|
||||||
as a best-effort optimization, but the error is propagated to ensure
|
as a best-effort optimization, but the error is propagated to ensure
|
||||||
@@ -459,11 +451,8 @@ async def upsert_chat_session(
|
|||||||
lock = await _get_session_lock(session.session_id)
|
lock = await _get_session_lock(session.session_id)
|
||||||
|
|
||||||
async with lock:
|
async with lock:
|
||||||
# Get existing message count from DB for incremental saves
|
# Always query DB for existing message count to ensure consistency
|
||||||
if existing_message_count is None:
|
existing_message_count = await chat_db().get_next_sequence(session.session_id)
|
||||||
existing_message_count = await chat_db().get_chat_session_message_count(
|
|
||||||
session.session_id
|
|
||||||
)
|
|
||||||
|
|
||||||
db_error: Exception | None = None
|
db_error: Exception | None = None
|
||||||
|
|
||||||
@@ -587,9 +576,7 @@ async def append_and_save_message(session_id: str, message: ChatMessage) -> Chat
|
|||||||
raise ValueError(f"Session {session_id} not found")
|
raise ValueError(f"Session {session_id} not found")
|
||||||
|
|
||||||
session.messages.append(message)
|
session.messages.append(message)
|
||||||
existing_message_count = await chat_db().get_chat_session_message_count(
|
existing_message_count = await chat_db().get_next_sequence(session_id)
|
||||||
session_id
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await _save_session_to_db(session, existing_message_count)
|
await _save_session_to_db(session, existing_message_count)
|
||||||
|
|||||||
@@ -331,3 +331,96 @@ def test_to_openai_messages_merges_split_assistants():
|
|||||||
tc_list = merged.get("tool_calls")
|
tc_list = merged.get("tool_calls")
|
||||||
assert tc_list is not None and len(list(tc_list)) == 1
|
assert tc_list is not None and len(list(tc_list)) == 1
|
||||||
assert list(tc_list)[0]["id"] == "tc1"
|
assert list(tc_list)[0]["id"] == "tc1"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Concurrent save collision detection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio(loop_scope="session")
|
||||||
|
async def test_concurrent_saves_collision_detection(setup_test_user, test_user_id):
|
||||||
|
"""Test that concurrent saves from streaming loop and callback handle collisions correctly.
|
||||||
|
|
||||||
|
Simulates the race condition where:
|
||||||
|
1. Streaming loop starts with saved_msg_count=5
|
||||||
|
2. Long-running callback appends message #5 and saves
|
||||||
|
3. Streaming loop tries to save with stale count=5
|
||||||
|
|
||||||
|
The collision detection should handle this gracefully.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# Create a session with initial messages
|
||||||
|
session = ChatSession.new(user_id=test_user_id)
|
||||||
|
for i in range(3):
|
||||||
|
session.messages.append(
|
||||||
|
ChatMessage(
|
||||||
|
role="user" if i % 2 == 0 else "assistant", content=f"Message {i}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save initial messages
|
||||||
|
session = await upsert_chat_session(session)
|
||||||
|
|
||||||
|
# Simulate streaming loop and callback saving concurrently
|
||||||
|
async def streaming_loop_save():
|
||||||
|
"""Simulates streaming loop saving messages."""
|
||||||
|
# Add 2 messages
|
||||||
|
session.messages.append(ChatMessage(role="user", content="Streaming message 1"))
|
||||||
|
session.messages.append(
|
||||||
|
ChatMessage(role="assistant", content="Streaming message 2")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait a bit to let callback potentially save first
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
# Save (will query DB for existing count)
|
||||||
|
return await upsert_chat_session(session)
|
||||||
|
|
||||||
|
async def callback_save():
|
||||||
|
"""Simulates long-running callback saving a message."""
|
||||||
|
# Add 1 message
|
||||||
|
session.messages.append(
|
||||||
|
ChatMessage(role="tool", content="Callback result", tool_call_id="tc1")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save immediately (will query DB for existing count)
|
||||||
|
return await upsert_chat_session(session)
|
||||||
|
|
||||||
|
# Run both saves concurrently - one will hit collision detection
|
||||||
|
results = await asyncio.gather(streaming_loop_save(), callback_save())
|
||||||
|
|
||||||
|
# Both should succeed
|
||||||
|
assert all(r is not None for r in results)
|
||||||
|
|
||||||
|
# Reload session from DB to verify
|
||||||
|
from backend.data.redis_client import get_redis_async
|
||||||
|
|
||||||
|
redis_key = f"chat:session:{session.session_id}"
|
||||||
|
async_redis = await get_redis_async()
|
||||||
|
await async_redis.delete(redis_key) # Clear cache to force DB load
|
||||||
|
|
||||||
|
loaded_session = await get_chat_session(session.session_id, test_user_id)
|
||||||
|
assert loaded_session is not None
|
||||||
|
|
||||||
|
# Should have all 6 messages (3 initial + 2 streaming + 1 callback)
|
||||||
|
assert len(loaded_session.messages) == 6
|
||||||
|
|
||||||
|
# Verify no duplicate sequences
|
||||||
|
sequences = []
|
||||||
|
for i, msg in enumerate(loaded_session.messages):
|
||||||
|
# Messages should have sequential sequence numbers starting from 0
|
||||||
|
sequences.append(i)
|
||||||
|
|
||||||
|
# All sequences should be unique and sequential
|
||||||
|
assert sequences == list(range(6))
|
||||||
|
|
||||||
|
# Verify message content is preserved
|
||||||
|
contents = [m.content for m in loaded_session.messages]
|
||||||
|
assert "Message 0" in contents
|
||||||
|
assert "Message 1" in contents
|
||||||
|
assert "Message 2" in contents
|
||||||
|
assert "Streaming message 1" in contents
|
||||||
|
assert "Streaming message 2" in contents
|
||||||
|
assert "Callback result" in contents
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ class ResponseType(str, Enum):
|
|||||||
TOOL_INPUT_AVAILABLE = "tool-input-available"
|
TOOL_INPUT_AVAILABLE = "tool-input-available"
|
||||||
TOOL_OUTPUT_AVAILABLE = "tool-output-available"
|
TOOL_OUTPUT_AVAILABLE = "tool-output-available"
|
||||||
|
|
||||||
# Long-running tool notification (custom extension - uses AI SDK DataUIPart format)
|
|
||||||
LONG_RUNNING_START = "data-long-running-start"
|
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
USAGE = "usage"
|
USAGE = "usage"
|
||||||
@@ -148,10 +145,6 @@ class StreamToolInputAvailable(StreamBaseResponse):
|
|||||||
input: dict[str, Any] = Field(
|
input: dict[str, Any] = Field(
|
||||||
default_factory=dict, description="Tool input arguments"
|
default_factory=dict, description="Tool input arguments"
|
||||||
)
|
)
|
||||||
providerMetadata: dict[str, Any] | None = Field(
|
|
||||||
default=None,
|
|
||||||
description="Provider metadata - used to pass isLongRunning flag to frontend",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StreamToolOutputAvailable(StreamBaseResponse):
|
class StreamToolOutputAvailable(StreamBaseResponse):
|
||||||
@@ -180,20 +173,6 @@ class StreamToolOutputAvailable(StreamBaseResponse):
|
|||||||
return f"data: {json.dumps(data)}\n\n"
|
return f"data: {json.dumps(data)}\n\n"
|
||||||
|
|
||||||
|
|
||||||
class StreamLongRunningStart(StreamBaseResponse):
|
|
||||||
"""Notification that a long-running tool has started.
|
|
||||||
|
|
||||||
Custom extension using AI SDK DataUIPart format. Signals the frontend to show
|
|
||||||
UI feedback while the tool executes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type: ResponseType = ResponseType.LONG_RUNNING_START
|
|
||||||
data: dict[str, Any] = Field(
|
|
||||||
default_factory=dict,
|
|
||||||
description="Data for the long-running event containing toolCallId and toolName",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ========== Other ==========
|
# ========== Other ==========
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ from backend.copilot.response_model import (
|
|||||||
StreamToolInputStart,
|
StreamToolInputStart,
|
||||||
StreamToolOutputAvailable,
|
StreamToolOutputAvailable,
|
||||||
)
|
)
|
||||||
from backend.copilot.tools import get_tool
|
|
||||||
|
|
||||||
from .tool_adapter import MCP_TOOL_PREFIX, pop_pending_tool_output
|
from .tool_adapter import MCP_TOOL_PREFIX, pop_pending_tool_output
|
||||||
|
|
||||||
@@ -112,15 +111,6 @@ class SDKResponseAdapter:
|
|||||||
# instead of "mcp__copilot__find_block".
|
# instead of "mcp__copilot__find_block".
|
||||||
tool_name = block.name.removeprefix(MCP_TOOL_PREFIX)
|
tool_name = block.name.removeprefix(MCP_TOOL_PREFIX)
|
||||||
|
|
||||||
# Check if this is a long-running tool to trigger UI feedback
|
|
||||||
tool = get_tool(tool_name)
|
|
||||||
is_long_running = tool.is_long_running if tool else False
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"[ADAPTER] Tool: {tool_name}, has_tool={tool is not None}, "
|
|
||||||
f"is_long_running={is_long_running}"
|
|
||||||
)
|
|
||||||
|
|
||||||
responses.append(
|
responses.append(
|
||||||
StreamToolInputStart(toolCallId=block.id, toolName=tool_name)
|
StreamToolInputStart(toolCallId=block.id, toolName=tool_name)
|
||||||
)
|
)
|
||||||
@@ -129,15 +119,8 @@ class SDKResponseAdapter:
|
|||||||
toolCallId=block.id,
|
toolCallId=block.id,
|
||||||
toolName=tool_name,
|
toolName=tool_name,
|
||||||
input=block.input,
|
input=block.input,
|
||||||
providerMetadata=(
|
|
||||||
{"isLongRunning": True} if is_long_running else None
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
logger.info(
|
|
||||||
f"[ADAPTER] Created StreamToolInputAvailable with "
|
|
||||||
f"providerMetadata={{'isLongRunning': {is_long_running}}}"
|
|
||||||
)
|
|
||||||
self.current_tool_calls[block.id] = {"name": tool_name}
|
self.current_tool_calls[block.id] = {"name": tool_name}
|
||||||
|
|
||||||
elif isinstance(sdk_message, UserMessage):
|
elif isinstance(sdk_message, UserMessage):
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import os
|
|||||||
import uuid
|
import uuid
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from backend.data.redis_client import get_redis_async
|
||||||
|
from backend.executor.cluster_lock import AsyncClusterLock
|
||||||
from backend.util.exceptions import NotFoundError
|
from backend.util.exceptions import NotFoundError
|
||||||
|
|
||||||
|
from .. import stream_registry
|
||||||
from ..config import ChatConfig
|
from ..config import ChatConfig
|
||||||
from ..model import (
|
from ..model import (
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
@@ -30,7 +33,12 @@ from ..response_model import (
|
|||||||
StreamToolInputAvailable,
|
StreamToolInputAvailable,
|
||||||
StreamToolOutputAvailable,
|
StreamToolOutputAvailable,
|
||||||
)
|
)
|
||||||
from ..service import _build_system_prompt, _generate_session_title
|
from ..service import (
|
||||||
|
_build_system_prompt,
|
||||||
|
_execute_long_running_tool_with_streaming,
|
||||||
|
_generate_session_title,
|
||||||
|
)
|
||||||
|
from ..tools.models import OperationPendingResponse, OperationStartedResponse
|
||||||
from ..tools.sandbox import WORKSPACE_PREFIX, make_session_path
|
from ..tools.sandbox import WORKSPACE_PREFIX, make_session_path
|
||||||
from ..tracking import track_user_message
|
from ..tracking import track_user_message
|
||||||
from .response_adapter import SDKResponseAdapter
|
from .response_adapter import SDKResponseAdapter
|
||||||
@@ -38,6 +46,7 @@ from .security_hooks import create_security_hooks
|
|||||||
from .tool_adapter import (
|
from .tool_adapter import (
|
||||||
COPILOT_TOOL_NAMES,
|
COPILOT_TOOL_NAMES,
|
||||||
SDK_DISALLOWED_TOOLS,
|
SDK_DISALLOWED_TOOLS,
|
||||||
|
LongRunningCallback,
|
||||||
create_copilot_mcp_server,
|
create_copilot_mcp_server,
|
||||||
set_execution_context,
|
set_execution_context,
|
||||||
wait_for_stash,
|
wait_for_stash,
|
||||||
@@ -54,6 +63,7 @@ from .transcript import (
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
config = ChatConfig()
|
config = ChatConfig()
|
||||||
|
|
||||||
|
|
||||||
# Set to hold background tasks to prevent garbage collection
|
# Set to hold background tasks to prevent garbage collection
|
||||||
_background_tasks: set[asyncio.Task[Any]] = set()
|
_background_tasks: set[asyncio.Task[Any]] = set()
|
||||||
|
|
||||||
@@ -116,15 +126,138 @@ When you create or modify important files (code, configs, outputs), you MUST:
|
|||||||
are available from previous turns
|
are available from previous turns
|
||||||
|
|
||||||
### Long-running tools
|
### Long-running tools
|
||||||
Long-running tools (create_agent, edit_agent, etc.) run synchronously
|
Long-running tools (create_agent, edit_agent, etc.) are handled
|
||||||
with heartbeats to keep the connection alive. The frontend shows UI feedback
|
asynchronously. You will receive an immediate response; the actual result
|
||||||
during execution based on stream events.
|
is delivered to the user via a background stream.
|
||||||
|
|
||||||
### Sub-agent tasks
|
### Sub-agent tasks
|
||||||
- When using the Task tool, NEVER set `run_in_background` to true.
|
- When using the Task tool, NEVER set `run_in_background` to true.
|
||||||
All tasks must run in the foreground.
|
All tasks must run in the foreground.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
STREAM_LOCK_PREFIX = "copilot:stream:lock:"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_long_running_callback(
|
||||||
|
user_id: str | None,
|
||||||
|
) -> LongRunningCallback:
|
||||||
|
"""Build a callback that delegates long-running tools to the non-SDK infrastructure.
|
||||||
|
|
||||||
|
Long-running tools (create_agent, edit_agent, etc.) are delegated to the
|
||||||
|
existing background infrastructure: stream_registry (Redis Streams),
|
||||||
|
database persistence, and SSE reconnection. This means results survive
|
||||||
|
page refreshes / pod restarts, and the frontend shows the proper loading
|
||||||
|
widget with progress updates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: User ID for the session
|
||||||
|
|
||||||
|
The returned callback matches the ``LongRunningCallback`` signature:
|
||||||
|
``(tool_name, args, session) -> MCP response dict``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def _callback(
|
||||||
|
tool_name: str, args: dict[str, Any], session: ChatSession
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
operation_id = str(uuid.uuid4())
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
tool_call_id = f"sdk-{uuid.uuid4().hex[:12]}"
|
||||||
|
session_id = session.session_id
|
||||||
|
|
||||||
|
# --- Build user-friendly messages (matches non-SDK service) ---
|
||||||
|
if tool_name == "create_agent":
|
||||||
|
desc = args.get("description", "")
|
||||||
|
desc_preview = (desc[:100] + "...") if len(desc) > 100 else desc
|
||||||
|
pending_msg = (
|
||||||
|
f"Creating your agent: {desc_preview}"
|
||||||
|
if desc_preview
|
||||||
|
else "Creating agent... This may take a few minutes."
|
||||||
|
)
|
||||||
|
started_msg = (
|
||||||
|
"Agent creation started. You can close this tab - "
|
||||||
|
"check your library in a few minutes."
|
||||||
|
)
|
||||||
|
elif tool_name == "edit_agent":
|
||||||
|
changes = args.get("changes", "")
|
||||||
|
changes_preview = (changes[:100] + "...") if len(changes) > 100 else changes
|
||||||
|
pending_msg = (
|
||||||
|
f"Editing agent: {changes_preview}"
|
||||||
|
if changes_preview
|
||||||
|
else "Editing agent... This may take a few minutes."
|
||||||
|
)
|
||||||
|
started_msg = (
|
||||||
|
"Agent edit started. You can close this tab - "
|
||||||
|
"check your library in a few minutes."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pending_msg = f"Running {tool_name}... This may take a few minutes."
|
||||||
|
started_msg = (
|
||||||
|
f"{tool_name} started. You can close this tab - "
|
||||||
|
"check back in a few minutes."
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Register task in Redis for SSE reconnection ---
|
||||||
|
await stream_registry.create_task(
|
||||||
|
task_id=task_id,
|
||||||
|
session_id=session_id,
|
||||||
|
user_id=user_id,
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
operation_id=operation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Save OperationPendingResponse to chat history ---
|
||||||
|
pending_message = ChatMessage(
|
||||||
|
role="tool",
|
||||||
|
content=OperationPendingResponse(
|
||||||
|
message=pending_msg,
|
||||||
|
operation_id=operation_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
).model_dump_json(),
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
)
|
||||||
|
session.messages.append(pending_message)
|
||||||
|
# Collision detection happens in add_chat_messages_batch (db.py)
|
||||||
|
session = await upsert_chat_session(session)
|
||||||
|
|
||||||
|
# --- Spawn background task (reuses non-SDK infrastructure) ---
|
||||||
|
bg_task = asyncio.create_task(
|
||||||
|
_execute_long_running_tool_with_streaming(
|
||||||
|
tool_name=tool_name,
|
||||||
|
parameters=args,
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
operation_id=operation_id,
|
||||||
|
task_id=task_id,
|
||||||
|
session_id=session_id,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_background_tasks.add(bg_task)
|
||||||
|
bg_task.add_done_callback(_background_tasks.discard)
|
||||||
|
await stream_registry.set_task_asyncio_task(task_id, bg_task)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[SDK] Long-running tool {tool_name} delegated to background "
|
||||||
|
f"(operation_id={operation_id}, task_id={task_id})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Return OperationStartedResponse as MCP tool result ---
|
||||||
|
# This flows through SDK → response adapter → frontend, triggering
|
||||||
|
# the loading widget with SSE reconnection support.
|
||||||
|
started_json = OperationStartedResponse(
|
||||||
|
message=started_msg,
|
||||||
|
operation_id=operation_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
task_id=task_id,
|
||||||
|
).model_dump_json()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"content": [{"type": "text", "text": started_json}],
|
||||||
|
"isError": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
return _callback
|
||||||
|
|
||||||
|
|
||||||
def _resolve_sdk_model() -> str | None:
|
def _resolve_sdk_model() -> str | None:
|
||||||
"""Resolve the model name for the Claude Agent SDK CLI.
|
"""Resolve the model name for the Claude Agent SDK CLI.
|
||||||
@@ -405,6 +538,9 @@ async def stream_chat_completion_sdk(
|
|||||||
f"Session {session_id} not found. Please create a new session first."
|
f"Session {session_id} not found. Please create a new session first."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Type narrowing: session is guaranteed ChatSession after the check above
|
||||||
|
session = cast(ChatSession, session)
|
||||||
|
|
||||||
# Append the new message to the session if it's not already there
|
# Append the new message to the session if it's not already there
|
||||||
new_message_role = "user" if is_user_message else "assistant"
|
new_message_role = "user" if is_user_message else "assistant"
|
||||||
if message and (
|
if message and (
|
||||||
@@ -442,6 +578,29 @@ async def stream_chat_completion_sdk(
|
|||||||
system_prompt += _SDK_TOOL_SUPPLEMENT
|
system_prompt += _SDK_TOOL_SUPPLEMENT
|
||||||
message_id = str(uuid.uuid4())
|
message_id = str(uuid.uuid4())
|
||||||
task_id = str(uuid.uuid4())
|
task_id = str(uuid.uuid4())
|
||||||
|
stream_id = task_id # Use task_id as unique stream identifier
|
||||||
|
|
||||||
|
# Acquire stream lock to prevent concurrent streams to the same session
|
||||||
|
lock = AsyncClusterLock(
|
||||||
|
redis=await get_redis_async(),
|
||||||
|
key=f"{STREAM_LOCK_PREFIX}{session_id}",
|
||||||
|
owner_id=stream_id,
|
||||||
|
timeout=config.stream_lock_ttl,
|
||||||
|
)
|
||||||
|
|
||||||
|
lock_owner = await lock.try_acquire()
|
||||||
|
if lock_owner != stream_id:
|
||||||
|
# Another stream is active
|
||||||
|
logger.warning(
|
||||||
|
f"[SDK] Session {session_id} already has an active stream: {lock_owner}"
|
||||||
|
)
|
||||||
|
yield StreamError(
|
||||||
|
errorText="Another stream is already active for this session. "
|
||||||
|
"Please wait or stop it.",
|
||||||
|
code="stream_already_active",
|
||||||
|
)
|
||||||
|
yield StreamFinish()
|
||||||
|
return
|
||||||
|
|
||||||
yield StreamStart(messageId=message_id, taskId=task_id)
|
yield StreamStart(messageId=message_id, taskId=task_id)
|
||||||
|
|
||||||
@@ -462,7 +621,7 @@ async def stream_chat_completion_sdk(
|
|||||||
set_execution_context(
|
set_execution_context(
|
||||||
user_id,
|
user_id,
|
||||||
session,
|
session,
|
||||||
long_running_callback=None,
|
long_running_callback=_build_long_running_callback(user_id),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
||||||
@@ -593,9 +752,6 @@ async def stream_chat_completion_sdk(
|
|||||||
accumulated_tool_calls: list[dict[str, Any]] = []
|
accumulated_tool_calls: list[dict[str, Any]] = []
|
||||||
has_appended_assistant = False
|
has_appended_assistant = False
|
||||||
has_tool_results = False
|
has_tool_results = False
|
||||||
# Track persisted message count to skip DB count queries
|
|
||||||
# on incremental saves. Initial save happened at line 545.
|
|
||||||
saved_msg_count = len(session.messages)
|
|
||||||
|
|
||||||
# Use an explicit async iterator with non-cancelling heartbeats.
|
# Use an explicit async iterator with non-cancelling heartbeats.
|
||||||
# CRITICAL: we must NOT cancel __anext__() mid-flight — doing so
|
# CRITICAL: we must NOT cancel __anext__() mid-flight — doing so
|
||||||
@@ -622,6 +778,8 @@ async def stream_chat_completion_sdk(
|
|||||||
|
|
||||||
if not done:
|
if not done:
|
||||||
# Timeout — emit heartbeat but keep the task alive
|
# Timeout — emit heartbeat but keep the task alive
|
||||||
|
# Also refresh lock TTL to keep it alive
|
||||||
|
await lock.refresh()
|
||||||
yield StreamHeartbeat()
|
yield StreamHeartbeat()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -771,13 +929,10 @@ async def stream_chat_completion_sdk(
|
|||||||
has_appended_assistant = True
|
has_appended_assistant = True
|
||||||
# Save before tool execution starts so the
|
# Save before tool execution starts so the
|
||||||
# pending tool call is visible on refresh /
|
# pending tool call is visible on refresh /
|
||||||
# other devices.
|
# other devices. Collision detection happens
|
||||||
|
# in add_chat_messages_batch (db.py).
|
||||||
try:
|
try:
|
||||||
await upsert_chat_session(
|
session = await upsert_chat_session(session)
|
||||||
session,
|
|
||||||
existing_message_count=saved_msg_count,
|
|
||||||
)
|
|
||||||
saved_msg_count = len(session.messages)
|
|
||||||
except Exception as save_err:
|
except Exception as save_err:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"[SDK] [%s] Incremental save " "failed: %s",
|
"[SDK] [%s] Incremental save " "failed: %s",
|
||||||
@@ -800,12 +955,9 @@ async def stream_chat_completion_sdk(
|
|||||||
has_tool_results = True
|
has_tool_results = True
|
||||||
# Save after tool completes so the result is
|
# Save after tool completes so the result is
|
||||||
# visible on refresh / other devices.
|
# visible on refresh / other devices.
|
||||||
|
# Collision detection happens in add_chat_messages_batch (db.py).
|
||||||
try:
|
try:
|
||||||
await upsert_chat_session(
|
session = await upsert_chat_session(session)
|
||||||
session,
|
|
||||||
existing_message_count=saved_msg_count,
|
|
||||||
)
|
|
||||||
saved_msg_count = len(session.messages)
|
|
||||||
except Exception as save_err:
|
except Exception as save_err:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"[SDK] [%s] Incremental save " "failed: %s",
|
"[SDK] [%s] Incremental save " "failed: %s",
|
||||||
@@ -937,7 +1089,7 @@ async def stream_chat_completion_sdk(
|
|||||||
"to use the OpenAI-compatible fallback."
|
"to use the OpenAI-compatible fallback."
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.shield(upsert_chat_session(session))
|
session = cast(ChatSession, await asyncio.shield(upsert_chat_session(session)))
|
||||||
logger.info(
|
logger.info(
|
||||||
"[SDK] [%s] Session saved with %d messages",
|
"[SDK] [%s] Session saved with %d messages",
|
||||||
session_id[:12],
|
session_id[:12],
|
||||||
@@ -954,10 +1106,11 @@ async def stream_chat_completion_sdk(
|
|||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[SDK] Error: {e}", exc_info=True)
|
logger.error(f"[SDK] Error: {e}", exc_info=True)
|
||||||
try:
|
if session:
|
||||||
await asyncio.shield(upsert_chat_session(session))
|
try:
|
||||||
except Exception as save_err:
|
await asyncio.shield(upsert_chat_session(session))
|
||||||
logger.error(f"[SDK] Failed to save session on error: {save_err}")
|
except Exception as save_err:
|
||||||
|
logger.error(f"[SDK] Failed to save session on error: {save_err}")
|
||||||
yield StreamError(
|
yield StreamError(
|
||||||
errorText="An error occurred. Please try again.",
|
errorText="An error occurred. Please try again.",
|
||||||
code="sdk_error",
|
code="sdk_error",
|
||||||
@@ -979,7 +1132,7 @@ async def stream_chat_completion_sdk(
|
|||||||
if not raw_transcript and use_resume and resume_file:
|
if not raw_transcript and use_resume and resume_file:
|
||||||
raw_transcript = read_transcript_file(resume_file)
|
raw_transcript = read_transcript_file(resume_file)
|
||||||
|
|
||||||
if raw_transcript:
|
if raw_transcript and session is not None:
|
||||||
await asyncio.shield(
|
await asyncio.shield(
|
||||||
_try_upload_transcript(
|
_try_upload_transcript(
|
||||||
user_id,
|
user_id,
|
||||||
@@ -999,6 +1152,9 @@ async def stream_chat_completion_sdk(
|
|||||||
if sdk_cwd:
|
if sdk_cwd:
|
||||||
_cleanup_sdk_tool_results(sdk_cwd)
|
_cleanup_sdk_tool_results(sdk_cwd)
|
||||||
|
|
||||||
|
# Release stream lock to allow new streams for this session
|
||||||
|
await lock.release()
|
||||||
|
|
||||||
|
|
||||||
async def _try_upload_transcript(
|
async def _try_upload_transcript(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ from .response_model import (
|
|||||||
StreamFinish,
|
StreamFinish,
|
||||||
StreamFinishStep,
|
StreamFinishStep,
|
||||||
StreamHeartbeat,
|
StreamHeartbeat,
|
||||||
StreamLongRunningStart,
|
|
||||||
StreamStart,
|
StreamStart,
|
||||||
StreamStartStep,
|
StreamStartStep,
|
||||||
StreamTextDelta,
|
StreamTextDelta,
|
||||||
@@ -64,7 +63,12 @@ from .response_model import (
|
|||||||
StreamUsage,
|
StreamUsage,
|
||||||
)
|
)
|
||||||
from .tools import execute_tool, get_tool, tools
|
from .tools import execute_tool, get_tool, tools
|
||||||
from .tools.models import ErrorResponse
|
from .tools.models import (
|
||||||
|
ErrorResponse,
|
||||||
|
OperationInProgressResponse,
|
||||||
|
OperationPendingResponse,
|
||||||
|
OperationStartedResponse,
|
||||||
|
)
|
||||||
from .tracking import track_user_message
|
from .tracking import track_user_message
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -348,7 +352,8 @@ async def assign_user_to_session(
|
|||||||
if not session:
|
if not session:
|
||||||
raise NotFoundError(f"Session {session_id} not found")
|
raise NotFoundError(f"Session {session_id} not found")
|
||||||
session.user_id = user_id
|
session.user_id = user_id
|
||||||
return await upsert_chat_session(session)
|
session = await upsert_chat_session(session)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
async def stream_chat_completion(
|
async def stream_chat_completion(
|
||||||
@@ -1398,15 +1403,16 @@ async def _yield_tool_call(
|
|||||||
"""
|
"""
|
||||||
Yield a tool call and its execution result.
|
Yield a tool call and its execution result.
|
||||||
|
|
||||||
Executes tools synchronously and yields heartbeat events every 15 seconds to
|
For tools marked with `is_long_running=True` (like agent generation), spawns a
|
||||||
keep the SSE connection alive during execution. The is_long_running property
|
background task so the operation survives SSE disconnections. For other tools,
|
||||||
is only used by the frontend to display UI feedback during long operations.
|
yields heartbeat events every 15 seconds to keep the SSE connection alive.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
orjson.JSONDecodeError: If tool call arguments cannot be parsed as JSON
|
orjson.JSONDecodeError: If tool call arguments cannot be parsed as JSON
|
||||||
KeyError: If expected tool call fields are missing
|
KeyError: If expected tool call fields are missing
|
||||||
TypeError: If tool call structure is invalid
|
TypeError: If tool call structure is invalid
|
||||||
"""
|
"""
|
||||||
|
import uuid as uuid_module
|
||||||
|
|
||||||
tool_name = tool_calls[yield_idx]["function"]["name"]
|
tool_name = tool_calls[yield_idx]["function"]["name"]
|
||||||
tool_call_id = tool_calls[yield_idx]["id"]
|
tool_call_id = tool_calls[yield_idx]["id"]
|
||||||
@@ -1424,17 +1430,167 @@ async def _yield_tool_call(
|
|||||||
input=arguments,
|
input=arguments,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Notify frontend if this is a long-running tool (e.g., agent generation)
|
# Check if this tool is long-running (survives SSE disconnection)
|
||||||
tool = get_tool(tool_name)
|
tool = get_tool(tool_name)
|
||||||
if tool and tool.is_long_running:
|
if tool and tool.is_long_running:
|
||||||
yield StreamLongRunningStart(
|
# Atomic check-and-set: returns False if operation already running (lost race)
|
||||||
data={
|
if not await _mark_operation_started(tool_call_id):
|
||||||
"toolCallId": tool_call_id,
|
logger.info(
|
||||||
"toolName": tool_name,
|
f"Tool call {tool_call_id} already in progress, returning status"
|
||||||
}
|
)
|
||||||
)
|
# Build dynamic message based on tool name
|
||||||
|
if tool_name == "create_agent":
|
||||||
|
in_progress_msg = "Agent creation already in progress. Please wait..."
|
||||||
|
elif tool_name == "edit_agent":
|
||||||
|
in_progress_msg = "Agent edit already in progress. Please wait..."
|
||||||
|
else:
|
||||||
|
in_progress_msg = f"{tool_name} already in progress. Please wait..."
|
||||||
|
|
||||||
# Run tool execution synchronously with heartbeats
|
yield StreamToolOutputAvailable(
|
||||||
|
toolCallId=tool_call_id,
|
||||||
|
toolName=tool_name,
|
||||||
|
output=OperationInProgressResponse(
|
||||||
|
message=in_progress_msg,
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
).model_dump_json(),
|
||||||
|
success=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate operation ID and task ID
|
||||||
|
operation_id = str(uuid_module.uuid4())
|
||||||
|
task_id = str(uuid_module.uuid4())
|
||||||
|
|
||||||
|
# Build a user-friendly message based on tool and arguments
|
||||||
|
if tool_name == "create_agent":
|
||||||
|
agent_desc = arguments.get("description", "")
|
||||||
|
# Truncate long descriptions for the message
|
||||||
|
desc_preview = (
|
||||||
|
(agent_desc[:100] + "...") if len(agent_desc) > 100 else agent_desc
|
||||||
|
)
|
||||||
|
pending_msg = (
|
||||||
|
f"Creating your agent: {desc_preview}"
|
||||||
|
if desc_preview
|
||||||
|
else "Creating agent... This may take a few minutes."
|
||||||
|
)
|
||||||
|
started_msg = (
|
||||||
|
"Agent creation started. You can close this tab - "
|
||||||
|
"check your library in a few minutes."
|
||||||
|
)
|
||||||
|
elif tool_name == "edit_agent":
|
||||||
|
changes = arguments.get("changes", "")
|
||||||
|
changes_preview = (changes[:100] + "...") if len(changes) > 100 else changes
|
||||||
|
pending_msg = (
|
||||||
|
f"Editing agent: {changes_preview}"
|
||||||
|
if changes_preview
|
||||||
|
else "Editing agent... This may take a few minutes."
|
||||||
|
)
|
||||||
|
started_msg = (
|
||||||
|
"Agent edit started. You can close this tab - "
|
||||||
|
"check your library in a few minutes."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pending_msg = f"Running {tool_name}... This may take a few minutes."
|
||||||
|
started_msg = (
|
||||||
|
f"{tool_name} started. You can close this tab - "
|
||||||
|
"check back in a few minutes."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track appended message for rollback on failure
|
||||||
|
pending_message: ChatMessage | None = None
|
||||||
|
|
||||||
|
# Wrap session save and task creation in try-except to release lock on failure
|
||||||
|
try:
|
||||||
|
# Create task in stream registry for SSE reconnection support
|
||||||
|
await stream_registry.create_task(
|
||||||
|
task_id=task_id,
|
||||||
|
session_id=session.session_id,
|
||||||
|
user_id=session.user_id,
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
operation_id=operation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attach tool_call and save pending result — lock serialises
|
||||||
|
# concurrent session mutations during parallel execution.
|
||||||
|
async def _save_pending() -> None:
|
||||||
|
nonlocal pending_message
|
||||||
|
session.add_tool_call_to_current_turn(tool_calls[yield_idx])
|
||||||
|
pending_message = ChatMessage(
|
||||||
|
role="tool",
|
||||||
|
content=OperationPendingResponse(
|
||||||
|
message=pending_msg,
|
||||||
|
operation_id=operation_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
).model_dump_json(),
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
)
|
||||||
|
session.messages.append(pending_message)
|
||||||
|
await upsert_chat_session(session)
|
||||||
|
|
||||||
|
await _with_optional_lock(session_lock, _save_pending)
|
||||||
|
logger.info(
|
||||||
|
f"Saved pending operation {operation_id} (task_id={task_id}) "
|
||||||
|
f"for tool {tool_name} in session {session.session_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store task reference in module-level set to prevent GC before completion
|
||||||
|
bg_task = asyncio.create_task(
|
||||||
|
_execute_long_running_tool_with_streaming(
|
||||||
|
tool_name=tool_name,
|
||||||
|
parameters=arguments,
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
operation_id=operation_id,
|
||||||
|
task_id=task_id,
|
||||||
|
session_id=session.session_id,
|
||||||
|
user_id=session.user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_background_tasks.add(bg_task)
|
||||||
|
bg_task.add_done_callback(_background_tasks.discard)
|
||||||
|
|
||||||
|
# Associate the asyncio task with the stream registry task
|
||||||
|
await stream_registry.set_task_asyncio_task(task_id, bg_task)
|
||||||
|
except Exception as e:
|
||||||
|
# Roll back appended messages — use identity-based removal so
|
||||||
|
# it works even when other parallel tools have appended after us.
|
||||||
|
async def _rollback() -> None:
|
||||||
|
if pending_message and pending_message in session.messages:
|
||||||
|
session.messages.remove(pending_message)
|
||||||
|
|
||||||
|
await _with_optional_lock(session_lock, _rollback)
|
||||||
|
|
||||||
|
# Release the Redis lock since the background task won't be spawned
|
||||||
|
await _mark_operation_completed(tool_call_id)
|
||||||
|
# Mark stream registry task as failed if it was created
|
||||||
|
try:
|
||||||
|
await stream_registry.mark_task_completed(
|
||||||
|
task_id,
|
||||||
|
status="failed",
|
||||||
|
error_message=f"Failed to setup tool {tool_name}: {e}",
|
||||||
|
)
|
||||||
|
except Exception as mark_err:
|
||||||
|
logger.warning(f"Failed to mark task {task_id} as failed: {mark_err}")
|
||||||
|
logger.error(
|
||||||
|
f"Failed to setup long-running tool {tool_name}: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Return immediately - don't wait for completion
|
||||||
|
yield StreamToolOutputAvailable(
|
||||||
|
toolCallId=tool_call_id,
|
||||||
|
toolName=tool_name,
|
||||||
|
output=OperationStartedResponse(
|
||||||
|
message=started_msg,
|
||||||
|
operation_id=operation_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
task_id=task_id, # Include task_id for SSE reconnection
|
||||||
|
).model_dump_json(),
|
||||||
|
success=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Normal flow: Run tool execution in background task with heartbeats
|
||||||
tool_task = asyncio.create_task(
|
tool_task = asyncio.create_task(
|
||||||
execute_tool(
|
execute_tool(
|
||||||
tool_name=tool_name,
|
tool_name=tool_name,
|
||||||
@@ -1580,7 +1736,11 @@ async def _execute_long_running_tool_with_streaming(
|
|||||||
session = await get_chat_session(session_id, user_id)
|
session = await get_chat_session(session_id, user_id)
|
||||||
if not session:
|
if not session:
|
||||||
logger.error(f"Session {session_id} not found for background tool")
|
logger.error(f"Session {session_id} not found for background tool")
|
||||||
await stream_registry.mark_task_completed(task_id, status="failed")
|
await stream_registry.mark_task_completed(
|
||||||
|
task_id,
|
||||||
|
status="failed",
|
||||||
|
error_message=f"Session {session_id} not found",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Pass operation_id and task_id to the tool for async processing
|
# Pass operation_id and task_id to the tool for async processing
|
||||||
|
|||||||
@@ -644,6 +644,8 @@ async def _stream_listener(
|
|||||||
async def mark_task_completed(
|
async def mark_task_completed(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
status: Literal["completed", "failed"] = "completed",
|
status: Literal["completed", "failed"] = "completed",
|
||||||
|
*,
|
||||||
|
error_message: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Mark a task as completed and publish finish event.
|
"""Mark a task as completed and publish finish event.
|
||||||
|
|
||||||
@@ -654,6 +656,10 @@ async def mark_task_completed(
|
|||||||
Args:
|
Args:
|
||||||
task_id: Task ID to mark as completed
|
task_id: Task ID to mark as completed
|
||||||
status: Final status ("completed" or "failed")
|
status: Final status ("completed" or "failed")
|
||||||
|
error_message: If provided and status="failed", publish a StreamError
|
||||||
|
before StreamFinish so connected clients see why the task ended.
|
||||||
|
If not provided, no StreamError is published (caller should publish
|
||||||
|
manually if needed to avoid duplicates).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if task was newly marked completed, False if already completed/failed
|
True if task was newly marked completed, False if already completed/failed
|
||||||
@@ -669,6 +675,17 @@ async def mark_task_completed(
|
|||||||
logger.debug(f"Task {task_id} already completed/failed, skipping")
|
logger.debug(f"Task {task_id} already completed/failed, skipping")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Publish error event before finish so connected clients know WHY the
|
||||||
|
# task ended. Only publish if caller provided an explicit error message
|
||||||
|
# to avoid duplicates with code paths that manually publish StreamError.
|
||||||
|
# This is best-effort — if it fails, the StreamFinish still ensures
|
||||||
|
# listeners clean up.
|
||||||
|
if status == "failed" and error_message:
|
||||||
|
try:
|
||||||
|
await publish_chunk(task_id, StreamError(errorText=error_message))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to publish error event for task {task_id}: {e}")
|
||||||
|
|
||||||
# THEN publish finish event (best-effort - listeners can detect via status polling)
|
# THEN publish finish event (best-effort - listeners can detect via status polling)
|
||||||
try:
|
try:
|
||||||
await publish_chunk(task_id, StreamFinish())
|
await publish_chunk(task_id, StreamFinish())
|
||||||
@@ -821,27 +838,6 @@ async def get_active_task_for_session(
|
|||||||
if task_user_id and user_id != task_user_id:
|
if task_user_id and user_id != task_user_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Auto-expire stale tasks that exceeded stream_timeout
|
|
||||||
created_at_str = meta.get("created_at", "")
|
|
||||||
if created_at_str:
|
|
||||||
try:
|
|
||||||
created_at = datetime.fromisoformat(created_at_str)
|
|
||||||
age_seconds = (
|
|
||||||
datetime.now(timezone.utc) - created_at
|
|
||||||
).total_seconds()
|
|
||||||
if age_seconds > config.stream_timeout:
|
|
||||||
logger.warning(
|
|
||||||
f"[TASK_LOOKUP] Auto-expiring stale task {task_id[:8]}... "
|
|
||||||
f"(age={age_seconds:.0f}s > timeout={config.stream_timeout}s)"
|
|
||||||
)
|
|
||||||
await mark_task_completed(task_id, "failed")
|
|
||||||
continue
|
|
||||||
except (ValueError, TypeError) as exc:
|
|
||||||
logger.warning(
|
|
||||||
f"[TASK_LOOKUP] Failed to parse created_at "
|
|
||||||
f"for task {task_id[:8]}...: {exc}"
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[TASK_LOOKUP] Found running task {task_id[:8]}... for session {session_id[:8]}..."
|
f"[TASK_LOOKUP] Found running task {task_id[:8]}... for session {session_id[:8]}..."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -540,15 +540,21 @@ async def decompose_goal(
|
|||||||
async def generate_agent(
|
async def generate_agent(
|
||||||
instructions: DecompositionResult | dict[str, Any],
|
instructions: DecompositionResult | dict[str, Any],
|
||||||
library_agents: list[AgentSummary] | list[dict[str, Any]] | None = None,
|
library_agents: list[AgentSummary] | list[dict[str, Any]] | None = None,
|
||||||
|
operation_id: str | None = None,
|
||||||
|
task_id: str | None = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Generate agent JSON from instructions.
|
"""Generate agent JSON from instructions.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
instructions: Structured instructions from decompose_goal
|
instructions: Structured instructions from decompose_goal
|
||||||
library_agents: User's library agents available for sub-agent composition
|
library_agents: User's library agents available for sub-agent composition
|
||||||
|
operation_id: Operation ID for async processing (enables Redis Streams
|
||||||
|
completion notification)
|
||||||
|
task_id: Task ID for async processing (enables Redis Streams persistence
|
||||||
|
and SSE delivery)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Agent JSON dict, error dict {"type": "error", ...}, or None on error
|
Agent JSON dict, {"status": "accepted"} for async, error dict {"type": "error", ...}, or None on error
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
AgentGeneratorNotConfiguredError: If the external service is not configured.
|
AgentGeneratorNotConfiguredError: If the external service is not configured.
|
||||||
@@ -556,9 +562,13 @@ async def generate_agent(
|
|||||||
_check_service_configured()
|
_check_service_configured()
|
||||||
logger.info("Calling external Agent Generator service for generate_agent")
|
logger.info("Calling external Agent Generator service for generate_agent")
|
||||||
result = await generate_agent_external(
|
result = await generate_agent_external(
|
||||||
dict(instructions), _to_dict_list(library_agents)
|
dict(instructions), _to_dict_list(library_agents), operation_id, task_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Don't modify async response
|
||||||
|
if result and result.get("status") == "accepted":
|
||||||
|
return result
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
if isinstance(result, dict) and result.get("type") == "error":
|
if isinstance(result, dict) and result.get("type") == "error":
|
||||||
return result
|
return result
|
||||||
@@ -749,6 +759,8 @@ async def generate_agent_patch(
|
|||||||
update_request: str,
|
update_request: str,
|
||||||
current_agent: dict[str, Any],
|
current_agent: dict[str, Any],
|
||||||
library_agents: list[AgentSummary] | None = None,
|
library_agents: list[AgentSummary] | None = None,
|
||||||
|
operation_id: str | None = None,
|
||||||
|
task_id: str | None = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Update an existing agent using natural language.
|
"""Update an existing agent using natural language.
|
||||||
|
|
||||||
@@ -761,10 +773,12 @@ async def generate_agent_patch(
|
|||||||
update_request: Natural language description of changes
|
update_request: Natural language description of changes
|
||||||
current_agent: Current agent JSON
|
current_agent: Current agent JSON
|
||||||
library_agents: User's library agents available for sub-agent composition
|
library_agents: User's library agents available for sub-agent composition
|
||||||
|
operation_id: Operation ID for async processing (enables Redis Streams callback)
|
||||||
|
task_id: Task ID for async processing (enables Redis Streams callback)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Updated agent JSON, clarifying questions dict {"type": "clarifying_questions", ...},
|
Updated agent JSON, clarifying questions dict {"type": "clarifying_questions", ...},
|
||||||
error dict {"type": "error", ...}, or None on error
|
{"status": "accepted"} for async, error dict {"type": "error", ...}, or None on error
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
AgentGeneratorNotConfiguredError: If the external service is not configured.
|
AgentGeneratorNotConfiguredError: If the external service is not configured.
|
||||||
@@ -775,6 +789,8 @@ async def generate_agent_patch(
|
|||||||
update_request,
|
update_request,
|
||||||
current_agent,
|
current_agent,
|
||||||
_to_dict_list(library_agents),
|
_to_dict_list(library_agents),
|
||||||
|
operation_id,
|
||||||
|
task_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ async def decompose_goal_dummy(
|
|||||||
async def generate_agent_dummy(
|
async def generate_agent_dummy(
|
||||||
instructions: dict[str, Any],
|
instructions: dict[str, Any],
|
||||||
library_agents: list[dict[str, Any]] | None = None,
|
library_agents: list[dict[str, Any]] | None = None,
|
||||||
|
operation_id: str | None = None,
|
||||||
|
task_id: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return dummy agent JSON after a simulated delay."""
|
"""Return dummy agent JSON after a simulated delay."""
|
||||||
logger.info("Using dummy agent generator for generate_agent (30s delay)")
|
logger.info("Using dummy agent generator for generate_agent (30s delay)")
|
||||||
@@ -112,6 +114,8 @@ async def generate_agent_patch_dummy(
|
|||||||
update_request: str,
|
update_request: str,
|
||||||
current_agent: dict[str, Any],
|
current_agent: dict[str, Any],
|
||||||
library_agents: list[dict[str, Any]] | None = None,
|
library_agents: list[dict[str, Any]] | None = None,
|
||||||
|
operation_id: str | None = None,
|
||||||
|
task_id: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return dummy patched agent (returns the current agent with updated description)."""
|
"""Return dummy patched agent (returns the current agent with updated description)."""
|
||||||
logger.info("Using dummy agent generator for generate_agent_patch")
|
logger.info("Using dummy agent generator for generate_agent_patch")
|
||||||
|
|||||||
@@ -242,18 +242,24 @@ async def decompose_goal_external(
|
|||||||
async def generate_agent_external(
|
async def generate_agent_external(
|
||||||
instructions: dict[str, Any],
|
instructions: dict[str, Any],
|
||||||
library_agents: list[dict[str, Any]] | None = None,
|
library_agents: list[dict[str, Any]] | None = None,
|
||||||
|
operation_id: str | None = None,
|
||||||
|
task_id: str | None = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Call the external service to generate an agent from instructions.
|
"""Call the external service to generate an agent from instructions.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
instructions: Structured instructions from decompose_goal
|
instructions: Structured instructions from decompose_goal
|
||||||
library_agents: User's library agents available for sub-agent composition
|
library_agents: User's library agents available for sub-agent composition
|
||||||
|
operation_id: Operation ID for async processing (enables Redis Streams callback)
|
||||||
|
task_id: Task ID for async processing (enables Redis Streams callback)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Agent JSON dict or error dict {"type": "error", ...} on error
|
Agent JSON dict, {"status": "accepted"} for async, or error dict {"type": "error", ...} on error
|
||||||
"""
|
"""
|
||||||
if _is_dummy_mode():
|
if _is_dummy_mode():
|
||||||
return await generate_agent_dummy(instructions, library_agents)
|
return await generate_agent_dummy(
|
||||||
|
instructions, library_agents, operation_id, task_id
|
||||||
|
)
|
||||||
|
|
||||||
client = _get_client()
|
client = _get_client()
|
||||||
|
|
||||||
@@ -261,9 +267,25 @@ async def generate_agent_external(
|
|||||||
payload: dict[str, Any] = {"instructions": instructions}
|
payload: dict[str, Any] = {"instructions": instructions}
|
||||||
if library_agents:
|
if library_agents:
|
||||||
payload["library_agents"] = library_agents
|
payload["library_agents"] = library_agents
|
||||||
|
if operation_id and task_id:
|
||||||
|
payload["operation_id"] = operation_id
|
||||||
|
payload["task_id"] = task_id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await client.post("/api/generate-agent", json=payload)
|
response = await client.post("/api/generate-agent", json=payload)
|
||||||
|
|
||||||
|
# Handle 202 Accepted for async processing
|
||||||
|
if response.status_code == 202:
|
||||||
|
logger.info(
|
||||||
|
f"Agent Generator accepted async request "
|
||||||
|
f"(operation_id={operation_id}, task_id={task_id})"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": "accepted",
|
||||||
|
"operation_id": operation_id,
|
||||||
|
"task_id": task_id,
|
||||||
|
}
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
@@ -295,6 +317,8 @@ async def generate_agent_patch_external(
|
|||||||
update_request: str,
|
update_request: str,
|
||||||
current_agent: dict[str, Any],
|
current_agent: dict[str, Any],
|
||||||
library_agents: list[dict[str, Any]] | None = None,
|
library_agents: list[dict[str, Any]] | None = None,
|
||||||
|
operation_id: str | None = None,
|
||||||
|
task_id: str | None = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Call the external service to generate a patch for an existing agent.
|
"""Call the external service to generate a patch for an existing agent.
|
||||||
|
|
||||||
@@ -302,13 +326,15 @@ async def generate_agent_patch_external(
|
|||||||
update_request: Natural language description of changes
|
update_request: Natural language description of changes
|
||||||
current_agent: Current agent JSON
|
current_agent: Current agent JSON
|
||||||
library_agents: User's library agents available for sub-agent composition
|
library_agents: User's library agents available for sub-agent composition
|
||||||
|
operation_id: Operation ID for async processing (enables Redis Streams callback)
|
||||||
|
task_id: Task ID for async processing (enables Redis Streams callback)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Updated agent JSON, clarifying questions dict, or error dict on error
|
Updated agent JSON, clarifying questions dict, {"status": "accepted"} for async, or error dict on error
|
||||||
"""
|
"""
|
||||||
if _is_dummy_mode():
|
if _is_dummy_mode():
|
||||||
return await generate_agent_patch_dummy(
|
return await generate_agent_patch_dummy(
|
||||||
update_request, current_agent, library_agents
|
update_request, current_agent, library_agents, operation_id, task_id
|
||||||
)
|
)
|
||||||
|
|
||||||
client = _get_client()
|
client = _get_client()
|
||||||
@@ -320,9 +346,25 @@ async def generate_agent_patch_external(
|
|||||||
}
|
}
|
||||||
if library_agents:
|
if library_agents:
|
||||||
payload["library_agents"] = library_agents
|
payload["library_agents"] = library_agents
|
||||||
|
if operation_id and task_id:
|
||||||
|
payload["operation_id"] = operation_id
|
||||||
|
payload["task_id"] = task_id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await client.post("/api/update-agent", json=payload)
|
response = await client.post("/api/update-agent", json=payload)
|
||||||
|
|
||||||
|
# Handle 202 Accepted for async processing
|
||||||
|
if response.status_code == 202:
|
||||||
|
logger.info(
|
||||||
|
f"Agent Generator accepted async update request "
|
||||||
|
f"(operation_id={operation_id}, task_id={task_id})"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": "accepted",
|
||||||
|
"operation_id": operation_id,
|
||||||
|
"task_id": task_id,
|
||||||
|
}
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,12 @@ class BaseTool:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_long_running(self) -> bool:
|
def is_long_running(self) -> bool:
|
||||||
"""Whether this tool takes a long time to execute (triggers long-running UI)."""
|
"""Whether this tool is long-running and should execute in background.
|
||||||
|
|
||||||
|
Long-running tools (like agent generation) are executed via background
|
||||||
|
tasks to survive SSE disconnections. The result is persisted to chat
|
||||||
|
history and visible when the user refreshes.
|
||||||
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def as_openai_tool(self) -> ChatCompletionToolParam:
|
def as_openai_tool(self) -> ChatCompletionToolParam:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from .base import BaseTool
|
|||||||
from .models import (
|
from .models import (
|
||||||
AgentPreviewResponse,
|
AgentPreviewResponse,
|
||||||
AgentSavedResponse,
|
AgentSavedResponse,
|
||||||
|
AsyncProcessingResponse,
|
||||||
ClarificationNeededResponse,
|
ClarificationNeededResponse,
|
||||||
ClarifyingQuestion,
|
ClarifyingQuestion,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
@@ -48,7 +49,6 @@ class CreateAgentTool(BaseTool):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_long_running(self) -> bool:
|
def is_long_running(self) -> bool:
|
||||||
"""Agent generation takes several minutes."""
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -100,6 +100,10 @@ class CreateAgentTool(BaseTool):
|
|||||||
save = kwargs.get("save", True)
|
save = kwargs.get("save", True)
|
||||||
session_id = session.session_id if session else None
|
session_id = session.session_id if session else None
|
||||||
|
|
||||||
|
# Extract async processing params (passed by long-running tool handler)
|
||||||
|
operation_id = kwargs.get("_operation_id")
|
||||||
|
task_id = kwargs.get("_task_id")
|
||||||
|
|
||||||
if not description:
|
if not description:
|
||||||
return ErrorResponse(
|
return ErrorResponse(
|
||||||
message="Please provide a description of what the agent should do.",
|
message="Please provide a description of what the agent should do.",
|
||||||
@@ -226,6 +230,8 @@ class CreateAgentTool(BaseTool):
|
|||||||
agent_json = await generate_agent(
|
agent_json = await generate_agent(
|
||||||
decomposition_result,
|
decomposition_result,
|
||||||
library_agents,
|
library_agents,
|
||||||
|
operation_id=operation_id,
|
||||||
|
task_id=task_id,
|
||||||
)
|
)
|
||||||
except AgentGeneratorNotConfiguredError:
|
except AgentGeneratorNotConfiguredError:
|
||||||
return ErrorResponse(
|
return ErrorResponse(
|
||||||
@@ -270,6 +276,19 @@ class CreateAgentTool(BaseTool):
|
|||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if Agent Generator accepted for async processing
|
||||||
|
if agent_json.get("status") == "accepted":
|
||||||
|
logger.info(
|
||||||
|
f"Agent generation delegated to async processing "
|
||||||
|
f"(operation_id={operation_id}, task_id={task_id})"
|
||||||
|
)
|
||||||
|
return AsyncProcessingResponse(
|
||||||
|
message="Agent generation started. You'll be notified when it's complete.",
|
||||||
|
operation_id=operation_id,
|
||||||
|
task_id=task_id,
|
||||||
|
session_id=session_id,
|
||||||
|
)
|
||||||
|
|
||||||
agent_name = agent_json.get("name", "Generated Agent")
|
agent_name = agent_json.get("name", "Generated Agent")
|
||||||
agent_description = agent_json.get("description", "")
|
agent_description = agent_json.get("description", "")
|
||||||
node_count = len(agent_json.get("nodes", []))
|
node_count = len(agent_json.get("nodes", []))
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ class CustomizeAgentTool(BaseTool):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_long_running(self) -> bool:
|
def is_long_running(self) -> bool:
|
||||||
"""Agent customization takes several minutes."""
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from .base import BaseTool
|
|||||||
from .models import (
|
from .models import (
|
||||||
AgentPreviewResponse,
|
AgentPreviewResponse,
|
||||||
AgentSavedResponse,
|
AgentSavedResponse,
|
||||||
|
AsyncProcessingResponse,
|
||||||
ClarificationNeededResponse,
|
ClarificationNeededResponse,
|
||||||
ClarifyingQuestion,
|
ClarifyingQuestion,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
@@ -46,7 +47,6 @@ class EditAgentTool(BaseTool):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_long_running(self) -> bool:
|
def is_long_running(self) -> bool:
|
||||||
"""Agent editing takes several minutes."""
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -105,6 +105,10 @@ class EditAgentTool(BaseTool):
|
|||||||
save = kwargs.get("save", True)
|
save = kwargs.get("save", True)
|
||||||
session_id = session.session_id if session else None
|
session_id = session.session_id if session else None
|
||||||
|
|
||||||
|
# Extract async processing params (passed by long-running tool handler)
|
||||||
|
operation_id = kwargs.get("_operation_id")
|
||||||
|
task_id = kwargs.get("_task_id")
|
||||||
|
|
||||||
if not agent_id:
|
if not agent_id:
|
||||||
return ErrorResponse(
|
return ErrorResponse(
|
||||||
message="Please provide the agent ID to edit.",
|
message="Please provide the agent ID to edit.",
|
||||||
@@ -153,6 +157,8 @@ class EditAgentTool(BaseTool):
|
|||||||
update_request,
|
update_request,
|
||||||
current_agent,
|
current_agent,
|
||||||
library_agents,
|
library_agents,
|
||||||
|
operation_id=operation_id,
|
||||||
|
task_id=task_id,
|
||||||
)
|
)
|
||||||
except AgentGeneratorNotConfiguredError:
|
except AgentGeneratorNotConfiguredError:
|
||||||
return ErrorResponse(
|
return ErrorResponse(
|
||||||
@@ -172,6 +178,19 @@ class EditAgentTool(BaseTool):
|
|||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if Agent Generator accepted for async processing
|
||||||
|
if result.get("status") == "accepted":
|
||||||
|
logger.info(
|
||||||
|
f"Agent edit delegated to async processing "
|
||||||
|
f"(operation_id={operation_id}, task_id={task_id})"
|
||||||
|
)
|
||||||
|
return AsyncProcessingResponse(
|
||||||
|
message="Agent edit started. You'll be notified when it's complete.",
|
||||||
|
operation_id=operation_id,
|
||||||
|
task_id=task_id,
|
||||||
|
session_id=session_id,
|
||||||
|
)
|
||||||
|
|
||||||
# Check if the result is an error from the external service
|
# Check if the result is an error from the external service
|
||||||
if isinstance(result, dict) and result.get("type") == "error":
|
if isinstance(result, dict) and result.get("type") == "error":
|
||||||
error_msg = result.get("error", "Unknown error")
|
error_msg = result.get("error", "Unknown error")
|
||||||
|
|||||||
@@ -459,6 +459,23 @@ class OperationInProgressResponse(ToolResponseBase):
|
|||||||
tool_call_id: str
|
tool_call_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncProcessingResponse(ToolResponseBase):
|
||||||
|
"""Response when an operation has been delegated to async processing.
|
||||||
|
|
||||||
|
This is returned by tools when the external service accepts the request
|
||||||
|
for async processing (HTTP 202 Accepted). The Redis Streams completion
|
||||||
|
consumer will handle the result when the external service completes.
|
||||||
|
|
||||||
|
The status field is specifically "accepted" to allow the long-running tool
|
||||||
|
handler to detect this response and skip LLM continuation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: ResponseType = ResponseType.OPERATION_STARTED
|
||||||
|
status: str = "accepted" # Must be "accepted" for detection
|
||||||
|
operation_id: str | None = None
|
||||||
|
task_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class WebFetchResponse(ToolResponseBase):
|
class WebFetchResponse(ToolResponseBase):
|
||||||
"""Response for web_fetch tool."""
|
"""Response for web_fetch tool."""
|
||||||
|
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ class DatabaseManager(AppService):
|
|||||||
get_user_chat_sessions = _(chat_db.get_user_chat_sessions)
|
get_user_chat_sessions = _(chat_db.get_user_chat_sessions)
|
||||||
get_user_session_count = _(chat_db.get_user_session_count)
|
get_user_session_count = _(chat_db.get_user_session_count)
|
||||||
delete_chat_session = _(chat_db.delete_chat_session)
|
delete_chat_session = _(chat_db.delete_chat_session)
|
||||||
get_chat_session_message_count = _(chat_db.get_chat_session_message_count)
|
get_next_sequence = _(chat_db.get_next_sequence)
|
||||||
update_tool_message_content = _(chat_db.update_tool_message_content)
|
update_tool_message_content = _(chat_db.update_tool_message_content)
|
||||||
|
|
||||||
|
|
||||||
@@ -473,5 +473,5 @@ class DatabaseManagerAsyncClient(AppServiceClient):
|
|||||||
get_user_chat_sessions = d.get_user_chat_sessions
|
get_user_chat_sessions = d.get_user_chat_sessions
|
||||||
get_user_session_count = d.get_user_session_count
|
get_user_session_count = d.get_user_session_count
|
||||||
delete_chat_session = d.delete_chat_session
|
delete_chat_session = d.delete_chat_session
|
||||||
get_chat_session_message_count = d.get_chat_session_message_count
|
get_next_sequence = d.get_next_sequence
|
||||||
update_tool_message_content = d.update_tool_message_content
|
update_tool_message_content = d.update_tool_message_content
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Redis-based distributed locking for cluster coordination."""
|
"""Redis-based distributed locking for cluster coordination."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -7,6 +8,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
from redis.asyncio import Redis as AsyncRedis
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -126,3 +128,124 @@ class ClusterLock:
|
|||||||
|
|
||||||
with self._refresh_lock:
|
with self._refresh_lock:
|
||||||
self._last_refresh = 0.0
|
self._last_refresh = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncClusterLock:
|
||||||
|
"""Async Redis-based distributed lock for preventing duplicate execution."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, redis: "AsyncRedis", key: str, owner_id: str, timeout: int = 300
|
||||||
|
):
|
||||||
|
self.redis = redis
|
||||||
|
self.key = key
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.timeout = timeout
|
||||||
|
self._last_refresh = 0.0
|
||||||
|
self._refresh_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def try_acquire(self) -> str | None:
|
||||||
|
"""Try to acquire the lock.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- owner_id (self.owner_id) if successfully acquired
|
||||||
|
- different owner_id if someone else holds the lock
|
||||||
|
- None if Redis is unavailable or other error
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
success = await self.redis.set(
|
||||||
|
self.key, self.owner_id, nx=True, ex=self.timeout
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
async with self._refresh_lock:
|
||||||
|
self._last_refresh = time.time()
|
||||||
|
return self.owner_id # Successfully acquired
|
||||||
|
|
||||||
|
# Failed to acquire, get current owner
|
||||||
|
current_value = await self.redis.get(self.key)
|
||||||
|
if current_value:
|
||||||
|
current_owner = (
|
||||||
|
current_value.decode("utf-8")
|
||||||
|
if isinstance(current_value, bytes)
|
||||||
|
else str(current_value)
|
||||||
|
)
|
||||||
|
return current_owner
|
||||||
|
|
||||||
|
# Key doesn't exist but we failed to set it - race condition or Redis issue
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AsyncClusterLock.try_acquire failed for key {self.key}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def refresh(self) -> bool:
|
||||||
|
"""Refresh lock TTL if we still own it.
|
||||||
|
|
||||||
|
Rate limited to at most once every timeout/10 seconds (minimum 1 second).
|
||||||
|
During rate limiting, still verifies lock existence but skips TTL extension.
|
||||||
|
Setting _last_refresh to 0 bypasses rate limiting for testing.
|
||||||
|
|
||||||
|
Async-safe: uses asyncio.Lock to protect _last_refresh access.
|
||||||
|
"""
|
||||||
|
# Calculate refresh interval: max(timeout // 10, 1)
|
||||||
|
refresh_interval = max(self.timeout // 10, 1)
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Check if we're within the rate limit period (async-safe read)
|
||||||
|
# _last_refresh == 0 forces a refresh (bypasses rate limiting for testing)
|
||||||
|
async with self._refresh_lock:
|
||||||
|
last_refresh = self._last_refresh
|
||||||
|
is_rate_limited = (
|
||||||
|
last_refresh > 0 and (current_time - last_refresh) < refresh_interval
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Always verify lock existence, even during rate limiting
|
||||||
|
current_value = await self.redis.get(self.key)
|
||||||
|
if not current_value:
|
||||||
|
async with self._refresh_lock:
|
||||||
|
self._last_refresh = 0
|
||||||
|
return False
|
||||||
|
|
||||||
|
stored_owner = (
|
||||||
|
current_value.decode("utf-8")
|
||||||
|
if isinstance(current_value, bytes)
|
||||||
|
else str(current_value)
|
||||||
|
)
|
||||||
|
if stored_owner != self.owner_id:
|
||||||
|
async with self._refresh_lock:
|
||||||
|
self._last_refresh = 0
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If rate limited, return True but don't update TTL or timestamp
|
||||||
|
if is_rate_limited:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Perform actual refresh
|
||||||
|
if await self.redis.expire(self.key, self.timeout):
|
||||||
|
async with self._refresh_lock:
|
||||||
|
self._last_refresh = current_time
|
||||||
|
return True
|
||||||
|
|
||||||
|
async with self._refresh_lock:
|
||||||
|
self._last_refresh = 0
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AsyncClusterLock.refresh failed for key {self.key}: {e}")
|
||||||
|
async with self._refresh_lock:
|
||||||
|
self._last_refresh = 0
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def release(self):
|
||||||
|
"""Release the lock."""
|
||||||
|
async with self._refresh_lock:
|
||||||
|
if self._last_refresh == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.redis.delete(self.key)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async with self._refresh_lock:
|
||||||
|
self._last_refresh = 0.0
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class TestGenerateAgent:
|
|||||||
instructions = {"type": "instructions", "steps": ["Step 1"]}
|
instructions = {"type": "instructions", "steps": ["Step 1"]}
|
||||||
result = await core.generate_agent(instructions)
|
result = await core.generate_agent(instructions)
|
||||||
|
|
||||||
mock_external.assert_called_once_with(instructions, None)
|
mock_external.assert_called_once_with(instructions, None, None, None)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result["name"] == "Test Agent"
|
assert result["name"] == "Test Agent"
|
||||||
assert "id" in result
|
assert "id" in result
|
||||||
@@ -173,7 +173,9 @@ class TestGenerateAgentPatch:
|
|||||||
current_agent = {"nodes": [], "links": []}
|
current_agent = {"nodes": [], "links": []}
|
||||||
result = await core.generate_agent_patch("Add a node", current_agent)
|
result = await core.generate_agent_patch("Add a node", current_agent)
|
||||||
|
|
||||||
mock_external.assert_called_once_with("Add a node", current_agent, None)
|
mock_external.assert_called_once_with(
|
||||||
|
"Add a node", current_agent, None, None, None
|
||||||
|
)
|
||||||
assert result == expected_result
|
assert result == expected_result
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ export const ChatContainer = ({
|
|||||||
onStop,
|
onStop,
|
||||||
headerSlot,
|
headerSlot,
|
||||||
}: ChatContainerProps) => {
|
}: ChatContainerProps) => {
|
||||||
const isBusy =
|
const isBusy = status === "streaming" || !!isReconnecting;
|
||||||
status === "streaming" || status === "submitted" || !!isReconnecting;
|
|
||||||
const inputLayoutId = "copilot-2-chat-input";
|
const inputLayoutId = "copilot-2-chat-input";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner
|
|||||||
import { toast } from "@/components/molecules/Toast/use-toast";
|
import { toast } from "@/components/molecules/Toast/use-toast";
|
||||||
import { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai";
|
import { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { ToolWrapper } from "../ToolWrapper/ToolWrapper";
|
|
||||||
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
|
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
|
||||||
import { EditAgentTool } from "../../tools/EditAgent/EditAgent";
|
import { EditAgentTool } from "../../tools/EditAgent/EditAgent";
|
||||||
import {
|
import {
|
||||||
@@ -209,110 +208,86 @@ export const ChatMessagesContainer = ({
|
|||||||
);
|
);
|
||||||
case "tool-find_block":
|
case "tool-find_block":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<FindBlocksTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<FindBlocksTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
case "tool-find_agent":
|
case "tool-find_agent":
|
||||||
case "tool-find_library_agent":
|
case "tool-find_library_agent":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<FindAgentsTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<FindAgentsTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
case "tool-search_docs":
|
case "tool-search_docs":
|
||||||
case "tool-get_doc_page":
|
case "tool-get_doc_page":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<SearchDocsTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<SearchDocsTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
case "tool-run_block":
|
case "tool-run_block":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<RunBlockTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<RunBlockTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
case "tool-run_agent":
|
case "tool-run_agent":
|
||||||
case "tool-schedule_agent":
|
case "tool-schedule_agent":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<RunAgentTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<RunAgentTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
case "tool-create_agent":
|
case "tool-create_agent":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<CreateAgentTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<CreateAgentTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
case "tool-edit_agent":
|
case "tool-edit_agent":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<EditAgentTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<EditAgentTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
case "tool-view_agent_output":
|
case "tool-view_agent_output":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<ViewAgentOutputTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<ViewAgentOutputTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
case "tool-search_feature_requests":
|
case "tool-search_feature_requests":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<SearchFeatureRequestsTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<SearchFeatureRequestsTool
|
|
||||||
part={part as ToolUIPart}
|
|
||||||
/>
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
case "tool-create_feature_request":
|
case "tool-create_feature_request":
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<CreateFeatureRequestTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<CreateFeatureRequestTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
// Render a generic tool indicator for SDK built-in
|
// Render a generic tool indicator for SDK built-in
|
||||||
// tools (Read, Glob, Grep, etc.) or any unrecognized tool
|
// tools (Read, Glob, Grep, etc.) or any unrecognized tool
|
||||||
if (part.type.startsWith("tool-")) {
|
if (part.type.startsWith("tool-")) {
|
||||||
return (
|
return (
|
||||||
<ToolWrapper
|
<GenericTool
|
||||||
key={`${message.id}-${i}`}
|
key={`${message.id}-${i}`}
|
||||||
part={part as ToolUIPart}
|
part={part as ToolUIPart}
|
||||||
>
|
/>
|
||||||
<GenericTool part={part as ToolUIPart} />
|
|
||||||
</ToolWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { PlusCircleIcon } from "@phosphor-icons/react";
|
|
||||||
import { ContentGrid, ContentHint } from "../ToolAccordion/AccordionContent";
|
|
||||||
import { ToolAccordion } from "../ToolAccordion/ToolAccordion";
|
|
||||||
import { MiniGame } from "../../tools/CreateAgent/components/MiniGame/MiniGame";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** Whether the tool is currently streaming/executing */
|
|
||||||
isStreaming: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays UI feedback while a long-running tool executes.
|
|
||||||
* Automatically shown for tools marked as is_long_running=True in the backend.
|
|
||||||
*/
|
|
||||||
export function LongRunningToolDisplay({ isStreaming }: Props) {
|
|
||||||
if (!isStreaming) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolAccordion
|
|
||||||
icon={<PlusCircleIcon size={32} weight="light" />}
|
|
||||||
title="This may take a few minutes. Play while you wait."
|
|
||||||
defaultExpanded={true}
|
|
||||||
>
|
|
||||||
<ContentGrid>
|
|
||||||
<MiniGame />
|
|
||||||
<ContentHint>
|
|
||||||
This could take a few minutes — play while you wait!
|
|
||||||
</ContentHint>
|
|
||||||
</ContentGrid>
|
|
||||||
</ToolAccordion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
@@ -1,52 +0,0 @@
|
|||||||
import type { ToolUIPart } from "ai";
|
|
||||||
import { LongRunningToolDisplay } from "../LongRunningToolDisplay/LongRunningToolDisplay";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
part: ToolUIPart;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for all tool components. Automatically shows UI feedback
|
|
||||||
* for long-running tools by detecting the isLongRunning flag on the tool part.
|
|
||||||
*/
|
|
||||||
export function ToolWrapper({ part, children }: Props) {
|
|
||||||
const isStreaming =
|
|
||||||
part.state === "input-streaming" || part.state === "input-available";
|
|
||||||
|
|
||||||
// Extract tool name from type (format: "tool-{name}")
|
|
||||||
const toolName = part.type.startsWith("tool-")
|
|
||||||
? part.type.substring(5)
|
|
||||||
: "unknown";
|
|
||||||
|
|
||||||
// Check if this tool is marked as long-running via providerMetadata
|
|
||||||
const isLongRunning =
|
|
||||||
"providerMetadata" in part &&
|
|
||||||
part.providerMetadata &&
|
|
||||||
typeof part.providerMetadata === "object" &&
|
|
||||||
"isLongRunning" in part.providerMetadata &&
|
|
||||||
part.providerMetadata.isLongRunning === true;
|
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
if (part.type.startsWith("tool-")) {
|
|
||||||
console.log("[ToolWrapper]", {
|
|
||||||
toolName,
|
|
||||||
type: part.type,
|
|
||||||
hasProviderMetadata: "providerMetadata" in part,
|
|
||||||
providerMetadata:
|
|
||||||
"providerMetadata" in part ? part.providerMetadata : undefined,
|
|
||||||
isLongRunning,
|
|
||||||
state: part.state,
|
|
||||||
isStreaming,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Show UI feedback if tool is long-running and streaming */}
|
|
||||||
{isLongRunning && <LongRunningToolDisplay isStreaming={isStreaming} />}
|
|
||||||
{/* Render the actual tool component */}
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,11 @@ import {
|
|||||||
MessageResponse,
|
MessageResponse,
|
||||||
} from "@/components/ai-elements/message";
|
} from "@/components/ai-elements/message";
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
import {
|
||||||
|
CredentialsProvidersContext,
|
||||||
|
type CredentialsProviderData,
|
||||||
|
type CredentialsProvidersContextType,
|
||||||
|
} from "@/providers/agent-credentials/credentials-provider";
|
||||||
import { CopilotChatActionsProvider } from "../components/CopilotChatActionsProvider/CopilotChatActionsProvider";
|
import { CopilotChatActionsProvider } from "../components/CopilotChatActionsProvider/CopilotChatActionsProvider";
|
||||||
import { CreateAgentTool } from "../tools/CreateAgent/CreateAgent";
|
import { CreateAgentTool } from "../tools/CreateAgent/CreateAgent";
|
||||||
import { EditAgentTool } from "../tools/EditAgent/EditAgent";
|
import { EditAgentTool } from "../tools/EditAgent/EditAgent";
|
||||||
@@ -97,6 +102,65 @@ function uid() {
|
|||||||
return `sg-${++_id}`;
|
return `sg-${++_id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mock credential providers for setup-requirements demos
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const noop = () => Promise.reject(new Error("Styleguide mock"));
|
||||||
|
|
||||||
|
function makeMockProvider(
|
||||||
|
provider: string,
|
||||||
|
providerName: string,
|
||||||
|
savedCredentials: CredentialsProviderData["savedCredentials"] = [],
|
||||||
|
): CredentialsProviderData {
|
||||||
|
return {
|
||||||
|
provider,
|
||||||
|
providerName,
|
||||||
|
savedCredentials,
|
||||||
|
isSystemProvider: false,
|
||||||
|
oAuthCallback: noop as CredentialsProviderData["oAuthCallback"],
|
||||||
|
mcpOAuthCallback: noop as CredentialsProviderData["mcpOAuthCallback"],
|
||||||
|
createAPIKeyCredentials:
|
||||||
|
noop as CredentialsProviderData["createAPIKeyCredentials"],
|
||||||
|
createUserPasswordCredentials:
|
||||||
|
noop as CredentialsProviderData["createUserPasswordCredentials"],
|
||||||
|
createHostScopedCredentials:
|
||||||
|
noop as CredentialsProviderData["createHostScopedCredentials"],
|
||||||
|
deleteCredentials: noop as CredentialsProviderData["deleteCredentials"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider context where the user already has saved credentials
|
||||||
|
* so the credential picker shows a selection list.
|
||||||
|
*/
|
||||||
|
const MOCK_PROVIDERS_WITH_CREDENTIALS: CredentialsProvidersContextType = {
|
||||||
|
google: makeMockProvider("google", "Google", [
|
||||||
|
{
|
||||||
|
id: "cred-google-1",
|
||||||
|
provider: "google",
|
||||||
|
type: "oauth2",
|
||||||
|
title: "work@company.com",
|
||||||
|
scopes: ["email", "calendar"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cred-google-2",
|
||||||
|
provider: "google",
|
||||||
|
type: "oauth2",
|
||||||
|
title: "personal@gmail.com",
|
||||||
|
scopes: ["email", "calendar"],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider context where the user has NO saved credentials,
|
||||||
|
* so the credential picker shows an "add new" flow.
|
||||||
|
*/
|
||||||
|
const MOCK_PROVIDERS_WITHOUT_CREDENTIALS: CredentialsProvidersContextType = {
|
||||||
|
openweathermap: makeMockProvider("openweathermap", "OpenWeatherMap"),
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Page
|
// Page
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -554,45 +618,80 @@ export default function StyleguidePage() {
|
|||||||
/>
|
/>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection label="Output available (setup requirements)">
|
<SubSection label="Setup requirements — no credentials (add new)">
|
||||||
<RunBlockTool
|
<CredentialsProvidersContext.Provider
|
||||||
part={{
|
value={MOCK_PROVIDERS_WITHOUT_CREDENTIALS}
|
||||||
type: "tool-run_block",
|
>
|
||||||
toolCallId: uid(),
|
<RunBlockTool
|
||||||
state: "output-available",
|
part={{
|
||||||
input: { block_id: "weather-block-123" },
|
type: "tool-run_block",
|
||||||
output: {
|
toolCallId: uid(),
|
||||||
type: ResponseType.setup_requirements,
|
state: "output-available",
|
||||||
message:
|
input: { block_id: "weather-block-123" },
|
||||||
"This block requires API credentials to run. Please configure them below.",
|
output: {
|
||||||
setup_info: {
|
type: ResponseType.setup_requirements,
|
||||||
agent_name: "Weather Agent",
|
message:
|
||||||
requirements: {
|
"This block requires API credentials to run. Please configure them below.",
|
||||||
inputs: [
|
setup_info: {
|
||||||
{
|
agent_id: "agent-weather-1",
|
||||||
name: "city",
|
agent_name: "Weather Agent",
|
||||||
title: "City",
|
requirements: {
|
||||||
type: "string",
|
inputs: [
|
||||||
required: true,
|
{
|
||||||
description: "The city to get weather for",
|
name: "city",
|
||||||
},
|
title: "City",
|
||||||
],
|
type: "string",
|
||||||
},
|
required: true,
|
||||||
user_readiness: {
|
description: "The city to get weather for",
|
||||||
missing_credentials: {
|
},
|
||||||
openweathermap: {
|
],
|
||||||
provider: "openweathermap",
|
},
|
||||||
credentials_type: "api_key",
|
user_readiness: {
|
||||||
title: "OpenWeatherMap API Key",
|
missing_credentials: {
|
||||||
description:
|
openweathermap_key: {
|
||||||
"Required to access weather data. Get your key at openweathermap.org",
|
provider: "openweathermap",
|
||||||
|
types: ["api_key"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</CredentialsProvidersContext.Provider>
|
||||||
|
</SubSection>
|
||||||
|
|
||||||
|
<SubSection label="Setup requirements — has credentials (pick from list)">
|
||||||
|
<CredentialsProvidersContext.Provider
|
||||||
|
value={MOCK_PROVIDERS_WITH_CREDENTIALS}
|
||||||
|
>
|
||||||
|
<RunBlockTool
|
||||||
|
part={{
|
||||||
|
type: "tool-run_block",
|
||||||
|
toolCallId: uid(),
|
||||||
|
state: "output-available",
|
||||||
|
input: { block_id: "calendar-block-456" },
|
||||||
|
output: {
|
||||||
|
type: ResponseType.setup_requirements,
|
||||||
|
message:
|
||||||
|
"This block requires Google credentials. Pick an account below or connect a new one.",
|
||||||
|
setup_info: {
|
||||||
|
agent_id: "agent-calendar-1",
|
||||||
|
agent_name: "Calendar Agent",
|
||||||
|
user_readiness: {
|
||||||
|
missing_credentials: {
|
||||||
|
google_oauth: {
|
||||||
|
provider: "google",
|
||||||
|
types: ["oauth2"],
|
||||||
|
scopes: ["email", "calendar"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CredentialsProvidersContext.Provider>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection label="Output available (error)">
|
<SubSection label="Output available (error)">
|
||||||
@@ -849,34 +948,71 @@ export default function StyleguidePage() {
|
|||||||
/>
|
/>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection label="Output available (setup requirements)">
|
<SubSection label="Setup requirements — no credentials (add new)">
|
||||||
<RunAgentTool
|
<CredentialsProvidersContext.Provider
|
||||||
part={{
|
value={MOCK_PROVIDERS_WITHOUT_CREDENTIALS}
|
||||||
type: "tool-run_agent",
|
>
|
||||||
toolCallId: uid(),
|
<RunAgentTool
|
||||||
state: "output-available",
|
part={{
|
||||||
input: { username_agent_slug: "creator/my-agent" },
|
type: "tool-run_agent",
|
||||||
output: {
|
toolCallId: uid(),
|
||||||
type: ResponseType.setup_requirements,
|
state: "output-available",
|
||||||
message: "This agent requires additional setup.",
|
input: { username_agent_slug: "creator/weather-agent" },
|
||||||
setup_info: {
|
output: {
|
||||||
agent_name: "YouTube Summarizer",
|
type: ResponseType.setup_requirements,
|
||||||
requirements: {},
|
message:
|
||||||
user_readiness: {
|
"This agent requires an API key. Add your credentials below.",
|
||||||
missing_credentials: {
|
setup_info: {
|
||||||
youtube_api: {
|
agent_id: "agent-weather-1",
|
||||||
provider: "youtube",
|
agent_name: "Weather Agent",
|
||||||
credentials_type: "api_key",
|
requirements: {},
|
||||||
title: "YouTube Data API Key",
|
user_readiness: {
|
||||||
description:
|
missing_credentials: {
|
||||||
"Required to access YouTube video data.",
|
openweathermap_key: {
|
||||||
|
provider: "openweathermap",
|
||||||
|
types: ["api_key"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</CredentialsProvidersContext.Provider>
|
||||||
|
</SubSection>
|
||||||
|
|
||||||
|
<SubSection label="Setup requirements — has credentials (pick from list)">
|
||||||
|
<CredentialsProvidersContext.Provider
|
||||||
|
value={MOCK_PROVIDERS_WITH_CREDENTIALS}
|
||||||
|
>
|
||||||
|
<RunAgentTool
|
||||||
|
part={{
|
||||||
|
type: "tool-run_agent",
|
||||||
|
toolCallId: uid(),
|
||||||
|
state: "output-available",
|
||||||
|
input: { username_agent_slug: "creator/calendar-agent" },
|
||||||
|
output: {
|
||||||
|
type: ResponseType.setup_requirements,
|
||||||
|
message:
|
||||||
|
"This agent needs Google credentials. Pick an account or connect a new one.",
|
||||||
|
setup_info: {
|
||||||
|
agent_id: "agent-calendar-1",
|
||||||
|
agent_name: "Google Calendar Agent",
|
||||||
|
requirements: {},
|
||||||
|
user_readiness: {
|
||||||
|
missing_credentials: {
|
||||||
|
google_oauth: {
|
||||||
|
provider: "google",
|
||||||
|
types: ["oauth2"],
|
||||||
|
scopes: ["email", "calendar"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CredentialsProvidersContext.Provider>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection label="Output available (need login)">
|
<SubSection label="Output available (need login)">
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import {
|
|||||||
ClarificationQuestionsCard,
|
ClarificationQuestionsCard,
|
||||||
ClarifyingQuestion,
|
ClarifyingQuestion,
|
||||||
} from "./components/ClarificationQuestionsCard";
|
} from "./components/ClarificationQuestionsCard";
|
||||||
import sparklesImg from "./components/MiniGame/assets/sparkles.png";
|
import sparklesImg from "../../components/MiniGame/assets/sparkles.png";
|
||||||
|
import { MiniGame } from "../../components/MiniGame/MiniGame";
|
||||||
import { SuggestedGoalCard } from "./components/SuggestedGoalCard";
|
import { SuggestedGoalCard } from "./components/SuggestedGoalCard";
|
||||||
import {
|
import {
|
||||||
AccordionIcon,
|
AccordionIcon,
|
||||||
@@ -34,6 +35,9 @@ import {
|
|||||||
isAgentSavedOutput,
|
isAgentSavedOutput,
|
||||||
isClarificationNeededOutput,
|
isClarificationNeededOutput,
|
||||||
isErrorOutput,
|
isErrorOutput,
|
||||||
|
isOperationInProgressOutput,
|
||||||
|
isOperationPendingOutput,
|
||||||
|
isOperationStartedOutput,
|
||||||
isSuggestedGoalOutput,
|
isSuggestedGoalOutput,
|
||||||
ToolIcon,
|
ToolIcon,
|
||||||
truncateText,
|
truncateText,
|
||||||
@@ -81,6 +85,16 @@ function getAccordionMeta(output: CreateAgentToolOutput) {
|
|||||||
expanded: true,
|
expanded: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
isOperationStartedOutput(output) ||
|
||||||
|
isOperationPendingOutput(output) ||
|
||||||
|
isOperationInProgressOutput(output)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
icon,
|
||||||
|
title: output.message || "Agent creation started",
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
icon: (
|
icon: (
|
||||||
<WarningDiamondIcon size={32} weight="light" className="text-red-500" />
|
<WarningDiamondIcon size={32} weight="light" className="text-red-500" />
|
||||||
@@ -102,10 +116,19 @@ export function CreateAgentTool({ part }: Props) {
|
|||||||
const isError =
|
const isError =
|
||||||
part.state === "output-error" || (!!output && isErrorOutput(output));
|
part.state === "output-error" || (!!output && isErrorOutput(output));
|
||||||
|
|
||||||
|
const isOperating =
|
||||||
|
!!output &&
|
||||||
|
(isOperationStartedOutput(output) ||
|
||||||
|
isOperationPendingOutput(output) ||
|
||||||
|
isOperationInProgressOutput(output));
|
||||||
|
|
||||||
const hasExpandableContent =
|
const hasExpandableContent =
|
||||||
part.state === "output-available" &&
|
part.state === "output-available" &&
|
||||||
!!output &&
|
!!output &&
|
||||||
(isAgentPreviewOutput(output) ||
|
(isOperationStartedOutput(output) ||
|
||||||
|
isOperationPendingOutput(output) ||
|
||||||
|
isOperationInProgressOutput(output) ||
|
||||||
|
isAgentPreviewOutput(output) ||
|
||||||
isAgentSavedOutput(output) ||
|
isAgentSavedOutput(output) ||
|
||||||
isClarificationNeededOutput(output) ||
|
isClarificationNeededOutput(output) ||
|
||||||
isSuggestedGoalOutput(output) ||
|
isSuggestedGoalOutput(output) ||
|
||||||
@@ -143,8 +166,24 @@ export function CreateAgentTool({ part }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isStreaming && (
|
||||||
|
<ToolAccordion
|
||||||
|
icon={<AccordionIcon />}
|
||||||
|
title="Creating agent, this may take a few minutes. Play while you wait."
|
||||||
|
expanded
|
||||||
|
>
|
||||||
|
<ContentGrid>
|
||||||
|
<MiniGame />
|
||||||
|
</ContentGrid>
|
||||||
|
</ToolAccordion>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasExpandableContent && output && (
|
{hasExpandableContent && output && (
|
||||||
<ToolAccordion {...getAccordionMeta(output)}>
|
<ToolAccordion {...getAccordionMeta(output)}>
|
||||||
|
{isOperating && output.message && (
|
||||||
|
<ContentMessage>{output.message}</ContentMessage>
|
||||||
|
)}
|
||||||
|
|
||||||
{isAgentSavedOutput(output) && (
|
{isAgentSavedOutput(output) && (
|
||||||
<div className="rounded-xl border border-border/60 bg-card p-4 shadow-sm">
|
<div className="rounded-xl border border-border/60 bg-card p-4 shadow-sm">
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import type { AgentPreviewResponse } from "@/app/api/__generated__/models/agentP
|
|||||||
import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse";
|
import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse";
|
||||||
import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse";
|
import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse";
|
||||||
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
|
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
|
||||||
|
import type { OperationInProgressResponse } from "@/app/api/__generated__/models/operationInProgressResponse";
|
||||||
|
import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse";
|
||||||
|
import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse";
|
||||||
import { ResponseType } from "@/app/api/__generated__/models/responseType";
|
import { ResponseType } from "@/app/api/__generated__/models/responseType";
|
||||||
import type { SuggestedGoalResponse } from "@/app/api/__generated__/models/suggestedGoalResponse";
|
import type { SuggestedGoalResponse } from "@/app/api/__generated__/models/suggestedGoalResponse";
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +16,9 @@ import type { ToolUIPart } from "ai";
|
|||||||
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
||||||
|
|
||||||
export type CreateAgentToolOutput =
|
export type CreateAgentToolOutput =
|
||||||
|
| OperationStartedResponse
|
||||||
|
| OperationPendingResponse
|
||||||
|
| OperationInProgressResponse
|
||||||
| AgentPreviewResponse
|
| AgentPreviewResponse
|
||||||
| AgentSavedResponse
|
| AgentSavedResponse
|
||||||
| ClarificationNeededResponse
|
| ClarificationNeededResponse
|
||||||
@@ -33,6 +39,9 @@ function parseOutput(output: unknown): CreateAgentToolOutput | null {
|
|||||||
if (typeof output === "object") {
|
if (typeof output === "object") {
|
||||||
const type = (output as { type?: unknown }).type;
|
const type = (output as { type?: unknown }).type;
|
||||||
if (
|
if (
|
||||||
|
type === ResponseType.operation_started ||
|
||||||
|
type === ResponseType.operation_pending ||
|
||||||
|
type === ResponseType.operation_in_progress ||
|
||||||
type === ResponseType.agent_preview ||
|
type === ResponseType.agent_preview ||
|
||||||
type === ResponseType.agent_saved ||
|
type === ResponseType.agent_saved ||
|
||||||
type === ResponseType.clarification_needed ||
|
type === ResponseType.clarification_needed ||
|
||||||
@@ -41,6 +50,9 @@ function parseOutput(output: unknown): CreateAgentToolOutput | null {
|
|||||||
) {
|
) {
|
||||||
return output as CreateAgentToolOutput;
|
return output as CreateAgentToolOutput;
|
||||||
}
|
}
|
||||||
|
if ("operation_id" in output && "tool_name" in output)
|
||||||
|
return output as OperationStartedResponse | OperationPendingResponse;
|
||||||
|
if ("tool_call_id" in output) return output as OperationInProgressResponse;
|
||||||
if ("agent_json" in output && "agent_name" in output)
|
if ("agent_json" in output && "agent_name" in output)
|
||||||
return output as AgentPreviewResponse;
|
return output as AgentPreviewResponse;
|
||||||
if ("agent_id" in output && "library_agent_id" in output)
|
if ("agent_id" in output && "library_agent_id" in output)
|
||||||
@@ -60,6 +72,30 @@ export function getCreateAgentToolOutput(
|
|||||||
return parseOutput((part as { output?: unknown }).output);
|
return parseOutput((part as { output?: unknown }).output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isOperationStartedOutput(
|
||||||
|
output: CreateAgentToolOutput,
|
||||||
|
): output is OperationStartedResponse {
|
||||||
|
return (
|
||||||
|
output.type === ResponseType.operation_started ||
|
||||||
|
("operation_id" in output && "tool_name" in output)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOperationPendingOutput(
|
||||||
|
output: CreateAgentToolOutput,
|
||||||
|
): output is OperationPendingResponse {
|
||||||
|
return output.type === ResponseType.operation_pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOperationInProgressOutput(
|
||||||
|
output: CreateAgentToolOutput,
|
||||||
|
): output is OperationInProgressResponse {
|
||||||
|
return (
|
||||||
|
output.type === ResponseType.operation_in_progress ||
|
||||||
|
"tool_call_id" in output
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isAgentPreviewOutput(
|
export function isAgentPreviewOutput(
|
||||||
output: CreateAgentToolOutput,
|
output: CreateAgentToolOutput,
|
||||||
): output is AgentPreviewResponse {
|
): output is AgentPreviewResponse {
|
||||||
@@ -108,6 +144,10 @@ export function getAnimationText(part: {
|
|||||||
case "output-available": {
|
case "output-available": {
|
||||||
const output = parseOutput(part.output);
|
const output = parseOutput(part.output);
|
||||||
if (!output) return "Creating a new agent";
|
if (!output) return "Creating a new agent";
|
||||||
|
if (isOperationStartedOutput(output)) return "Agent creation started";
|
||||||
|
if (isOperationPendingOutput(output)) return "Agent creation in progress";
|
||||||
|
if (isOperationInProgressOutput(output))
|
||||||
|
return "Agent creation already in progress";
|
||||||
if (isAgentSavedOutput(output)) return `Saved ${output.agent_name}`;
|
if (isAgentSavedOutput(output)) return `Saved ${output.agent_name}`;
|
||||||
if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`;
|
if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`;
|
||||||
if (isClarificationNeededOutput(output)) return "Needs clarification";
|
if (isClarificationNeededOutput(output)) return "Needs clarification";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ContentMessage,
|
ContentMessage,
|
||||||
} from "../../components/ToolAccordion/AccordionContent";
|
} from "../../components/ToolAccordion/AccordionContent";
|
||||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||||
|
import { MiniGame } from "../../components/MiniGame/MiniGame";
|
||||||
import {
|
import {
|
||||||
ClarificationQuestionsCard,
|
ClarificationQuestionsCard,
|
||||||
ClarifyingQuestion,
|
ClarifyingQuestion,
|
||||||
@@ -25,6 +26,9 @@ import {
|
|||||||
isAgentSavedOutput,
|
isAgentSavedOutput,
|
||||||
isClarificationNeededOutput,
|
isClarificationNeededOutput,
|
||||||
isErrorOutput,
|
isErrorOutput,
|
||||||
|
isOperationInProgressOutput,
|
||||||
|
isOperationPendingOutput,
|
||||||
|
isOperationStartedOutput,
|
||||||
ToolIcon,
|
ToolIcon,
|
||||||
truncateText,
|
truncateText,
|
||||||
type EditAgentToolOutput,
|
type EditAgentToolOutput,
|
||||||
@@ -69,6 +73,16 @@ function getAccordionMeta(output: EditAgentToolOutput): {
|
|||||||
description: `${questions.length} question${questions.length === 1 ? "" : "s"}`,
|
description: `${questions.length} question${questions.length === 1 ? "" : "s"}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
isOperationStartedOutput(output) ||
|
||||||
|
isOperationPendingOutput(output) ||
|
||||||
|
isOperationInProgressOutput(output)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
icon,
|
||||||
|
title: output.message || "Agent editing started",
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
icon: (
|
icon: (
|
||||||
<WarningDiamondIcon size={32} weight="light" className="text-red-500" />
|
<WarningDiamondIcon size={32} weight="light" className="text-red-500" />
|
||||||
@@ -87,10 +101,18 @@ export function EditAgentTool({ part }: Props) {
|
|||||||
const output = getEditAgentToolOutput(part);
|
const output = getEditAgentToolOutput(part);
|
||||||
const isError =
|
const isError =
|
||||||
part.state === "output-error" || (!!output && isErrorOutput(output));
|
part.state === "output-error" || (!!output && isErrorOutput(output));
|
||||||
|
const isOperating =
|
||||||
|
!!output &&
|
||||||
|
(isOperationStartedOutput(output) ||
|
||||||
|
isOperationPendingOutput(output) ||
|
||||||
|
isOperationInProgressOutput(output));
|
||||||
const hasExpandableContent =
|
const hasExpandableContent =
|
||||||
part.state === "output-available" &&
|
part.state === "output-available" &&
|
||||||
!!output &&
|
!!output &&
|
||||||
(isAgentPreviewOutput(output) ||
|
(isOperationStartedOutput(output) ||
|
||||||
|
isOperationPendingOutput(output) ||
|
||||||
|
isOperationInProgressOutput(output) ||
|
||||||
|
isAgentPreviewOutput(output) ||
|
||||||
isAgentSavedOutput(output) ||
|
isAgentSavedOutput(output) ||
|
||||||
isClarificationNeededOutput(output) ||
|
isClarificationNeededOutput(output) ||
|
||||||
isErrorOutput(output));
|
isErrorOutput(output));
|
||||||
@@ -123,8 +145,24 @@ export function EditAgentTool({ part }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isStreaming && (
|
||||||
|
<ToolAccordion
|
||||||
|
icon={<AccordionIcon />}
|
||||||
|
title="Editing agent, this may take a few minutes. Play while you wait."
|
||||||
|
expanded
|
||||||
|
>
|
||||||
|
<ContentGrid>
|
||||||
|
<MiniGame />
|
||||||
|
</ContentGrid>
|
||||||
|
</ToolAccordion>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasExpandableContent && output && (
|
{hasExpandableContent && output && (
|
||||||
<ToolAccordion {...getAccordionMeta(output)}>
|
<ToolAccordion {...getAccordionMeta(output)}>
|
||||||
|
{isOperating && output.message && (
|
||||||
|
<ContentMessage>{output.message}</ContentMessage>
|
||||||
|
)}
|
||||||
|
|
||||||
{isAgentSavedOutput(output) && (
|
{isAgentSavedOutput(output) && (
|
||||||
<ContentGrid>
|
<ContentGrid>
|
||||||
<ContentMessage>{output.message}</ContentMessage>
|
<ContentMessage>{output.message}</ContentMessage>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import type { AgentPreviewResponse } from "@/app/api/__generated__/models/agentP
|
|||||||
import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse";
|
import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse";
|
||||||
import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse";
|
import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse";
|
||||||
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
|
import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
|
||||||
|
import type { OperationInProgressResponse } from "@/app/api/__generated__/models/operationInProgressResponse";
|
||||||
|
import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse";
|
||||||
|
import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse";
|
||||||
import { ResponseType } from "@/app/api/__generated__/models/responseType";
|
import { ResponseType } from "@/app/api/__generated__/models/responseType";
|
||||||
import {
|
import {
|
||||||
NotePencilIcon,
|
NotePencilIcon,
|
||||||
@@ -12,6 +15,9 @@ import type { ToolUIPart } from "ai";
|
|||||||
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
|
||||||
|
|
||||||
export type EditAgentToolOutput =
|
export type EditAgentToolOutput =
|
||||||
|
| OperationStartedResponse
|
||||||
|
| OperationPendingResponse
|
||||||
|
| OperationInProgressResponse
|
||||||
| AgentPreviewResponse
|
| AgentPreviewResponse
|
||||||
| AgentSavedResponse
|
| AgentSavedResponse
|
||||||
| ClarificationNeededResponse
|
| ClarificationNeededResponse
|
||||||
@@ -32,6 +38,8 @@ function parseOutput(output: unknown): EditAgentToolOutput | null {
|
|||||||
const type = (output as { type?: unknown }).type;
|
const type = (output as { type?: unknown }).type;
|
||||||
if (
|
if (
|
||||||
type === ResponseType.operation_started ||
|
type === ResponseType.operation_started ||
|
||||||
|
type === ResponseType.operation_pending ||
|
||||||
|
type === ResponseType.operation_in_progress ||
|
||||||
type === ResponseType.agent_preview ||
|
type === ResponseType.agent_preview ||
|
||||||
type === ResponseType.agent_saved ||
|
type === ResponseType.agent_saved ||
|
||||||
type === ResponseType.clarification_needed ||
|
type === ResponseType.clarification_needed ||
|
||||||
@@ -39,6 +47,9 @@ function parseOutput(output: unknown): EditAgentToolOutput | null {
|
|||||||
) {
|
) {
|
||||||
return output as EditAgentToolOutput;
|
return output as EditAgentToolOutput;
|
||||||
}
|
}
|
||||||
|
if ("operation_id" in output && "tool_name" in output)
|
||||||
|
return output as OperationStartedResponse | OperationPendingResponse;
|
||||||
|
if ("tool_call_id" in output) return output as OperationInProgressResponse;
|
||||||
if ("agent_json" in output && "agent_name" in output)
|
if ("agent_json" in output && "agent_name" in output)
|
||||||
return output as AgentPreviewResponse;
|
return output as AgentPreviewResponse;
|
||||||
if ("agent_id" in output && "library_agent_id" in output)
|
if ("agent_id" in output && "library_agent_id" in output)
|
||||||
@@ -57,6 +68,30 @@ export function getEditAgentToolOutput(
|
|||||||
return parseOutput((part as { output?: unknown }).output);
|
return parseOutput((part as { output?: unknown }).output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isOperationStartedOutput(
|
||||||
|
output: EditAgentToolOutput,
|
||||||
|
): output is OperationStartedResponse {
|
||||||
|
return (
|
||||||
|
output.type === ResponseType.operation_started ||
|
||||||
|
("operation_id" in output && "tool_name" in output)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOperationPendingOutput(
|
||||||
|
output: EditAgentToolOutput,
|
||||||
|
): output is OperationPendingResponse {
|
||||||
|
return output.type === ResponseType.operation_pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOperationInProgressOutput(
|
||||||
|
output: EditAgentToolOutput,
|
||||||
|
): output is OperationInProgressResponse {
|
||||||
|
return (
|
||||||
|
output.type === ResponseType.operation_in_progress ||
|
||||||
|
"tool_call_id" in output
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isAgentPreviewOutput(
|
export function isAgentPreviewOutput(
|
||||||
output: EditAgentToolOutput,
|
output: EditAgentToolOutput,
|
||||||
): output is AgentPreviewResponse {
|
): output is AgentPreviewResponse {
|
||||||
@@ -97,6 +132,10 @@ export function getAnimationText(part: {
|
|||||||
case "output-available": {
|
case "output-available": {
|
||||||
const output = parseOutput(part.output);
|
const output = parseOutput(part.output);
|
||||||
if (!output) return "Editing the agent";
|
if (!output) return "Editing the agent";
|
||||||
|
if (isOperationStartedOutput(output)) return "Agent update started";
|
||||||
|
if (isOperationPendingOutput(output)) return "Agent update in progress";
|
||||||
|
if (isOperationInProgressOutput(output))
|
||||||
|
return "Agent update already in progress";
|
||||||
if (isAgentSavedOutput(output)) return `Saved "${output.agent_name}"`;
|
if (isAgentSavedOutput(output)) return `Saved "${output.agent_name}"`;
|
||||||
if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`;
|
if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`;
|
||||||
if (isClarificationNeededOutput(output)) return "Needs clarification";
|
if (isClarificationNeededOutput(output)) return "Needs clarification";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ContentHint,
|
ContentHint,
|
||||||
ContentMessage,
|
ContentMessage,
|
||||||
} from "../../components/ToolAccordion/AccordionContent";
|
} from "../../components/ToolAccordion/AccordionContent";
|
||||||
import { MiniGame } from "../CreateAgent/components/MiniGame/MiniGame";
|
import { MiniGame } from "../../components/MiniGame/MiniGame";
|
||||||
import {
|
import {
|
||||||
getAccordionMeta,
|
getAccordionMeta,
|
||||||
getAnimationText,
|
getAnimationText,
|
||||||
@@ -47,14 +47,25 @@ export function RunAgentTool({ part }: Props) {
|
|||||||
const isError =
|
const isError =
|
||||||
part.state === "output-error" ||
|
part.state === "output-error" ||
|
||||||
(!!output && isRunAgentErrorOutput(output));
|
(!!output && isRunAgentErrorOutput(output));
|
||||||
|
const isOutputAvailable = part.state === "output-available" && !!output;
|
||||||
|
|
||||||
|
const setupRequirementsOutput =
|
||||||
|
isOutputAvailable && isRunAgentSetupRequirementsOutput(output)
|
||||||
|
? output
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const agentDetailsOutput =
|
||||||
|
isOutputAvailable && isRunAgentAgentDetailsOutput(output) ? output : null;
|
||||||
|
|
||||||
|
const needLoginOutput =
|
||||||
|
isOutputAvailable && isRunAgentNeedLoginOutput(output) ? output : null;
|
||||||
|
|
||||||
const hasExpandableContent =
|
const hasExpandableContent =
|
||||||
part.state === "output-available" &&
|
isOutputAvailable &&
|
||||||
!!output &&
|
!setupRequirementsOutput &&
|
||||||
(isRunAgentExecutionStartedOutput(output) ||
|
!agentDetailsOutput &&
|
||||||
isRunAgentAgentDetailsOutput(output) ||
|
!needLoginOutput &&
|
||||||
isRunAgentSetupRequirementsOutput(output) ||
|
(isRunAgentExecutionStartedOutput(output) || isRunAgentErrorOutput(output));
|
||||||
isRunAgentNeedLoginOutput(output) ||
|
|
||||||
isRunAgentErrorOutput(output));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
@@ -81,24 +92,30 @@ export function RunAgentTool({ part }: Props) {
|
|||||||
</ToolAccordion>
|
</ToolAccordion>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{setupRequirementsOutput && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<SetupRequirementsCard output={setupRequirementsOutput} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{agentDetailsOutput && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<AgentDetailsCard output={agentDetailsOutput} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{needLoginOutput && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<ContentMessage>{needLoginOutput.message}</ContentMessage>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasExpandableContent && output && (
|
{hasExpandableContent && output && (
|
||||||
<ToolAccordion {...getAccordionMeta(output)}>
|
<ToolAccordion {...getAccordionMeta(output)}>
|
||||||
{isRunAgentExecutionStartedOutput(output) && (
|
{isRunAgentExecutionStartedOutput(output) && (
|
||||||
<ExecutionStartedCard output={output} />
|
<ExecutionStartedCard output={output} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isRunAgentAgentDetailsOutput(output) && (
|
|
||||||
<AgentDetailsCard output={output} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isRunAgentSetupRequirementsOutput(output) && (
|
|
||||||
<SetupRequirementsCard output={output} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isRunAgentNeedLoginOutput(output) && (
|
|
||||||
<ContentMessage>{output.message}</ContentMessage>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isRunAgentErrorOutput(output) && <ErrorCard output={output} />}
|
{isRunAgentErrorOutput(output) && <ErrorCard output={output} />}
|
||||||
</ToolAccordion>
|
</ToolAccordion>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
|
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
|
||||||
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
|
||||||
import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
|
import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
|
||||||
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
|
||||||
|
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||||
|
import { useState } from "react";
|
||||||
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||||
import {
|
import {
|
||||||
ContentBadge,
|
ContentBadge,
|
||||||
@@ -38,40 +39,40 @@ export function SetupRequirementsCard({ output }: Props) {
|
|||||||
setInputCredentials((prev) => ({ ...prev, [key]: value }));
|
setInputCredentials((prev) => ({ ...prev, [key]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAllComplete =
|
const needsCredentials = credentialFields.length > 0;
|
||||||
credentialFields.length > 0 &&
|
const isAllCredentialsComplete =
|
||||||
|
needsCredentials &&
|
||||||
[...requiredCredentials].every((key) => !!inputCredentials[key]);
|
[...requiredCredentials].every((key) => !!inputCredentials[key]);
|
||||||
|
|
||||||
|
const canProceed =
|
||||||
|
!hasSent && (!needsCredentials || isAllCredentialsComplete);
|
||||||
|
|
||||||
function handleProceed() {
|
function handleProceed() {
|
||||||
setHasSent(true);
|
setHasSent(true);
|
||||||
onSend(
|
const message = needsCredentials
|
||||||
"I've configured the required credentials. Please check if everything is ready and proceed with running the agent.",
|
? "I've configured the required credentials. Please check if everything is ready and proceed with running the agent."
|
||||||
);
|
: "Please proceed with running the agent.";
|
||||||
|
onSend(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<ContentMessage>{output.message}</ContentMessage>
|
<ContentMessage>{output.message}</ContentMessage>
|
||||||
|
|
||||||
{credentialFields.length > 0 && (
|
{needsCredentials && (
|
||||||
<div className="rounded-2xl border bg-background p-3">
|
<div className="rounded-2xl border bg-background p-3">
|
||||||
<CredentialsGroupedView
|
<Text variant="small" className="w-fit border-b text-zinc-500">
|
||||||
credentialFields={credentialFields}
|
Agent credentials
|
||||||
requiredCredentials={requiredCredentials}
|
</Text>
|
||||||
inputCredentials={inputCredentials}
|
<div className="mt-6">
|
||||||
inputValues={{}}
|
<CredentialsGroupedView
|
||||||
onCredentialChange={handleCredentialChange}
|
credentialFields={credentialFields}
|
||||||
/>
|
requiredCredentials={requiredCredentials}
|
||||||
{isAllComplete && !hasSent && (
|
inputCredentials={inputCredentials}
|
||||||
<Button
|
inputValues={{}}
|
||||||
variant="primary"
|
onCredentialChange={handleCredentialChange}
|
||||||
size="small"
|
/>
|
||||||
className="mt-3 w-full"
|
</div>
|
||||||
onClick={handleProceed}
|
|
||||||
>
|
|
||||||
Proceed
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -100,6 +101,18 @@ export function SetupRequirementsCard({ output }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(needsCredentials || expectedInputs.length > 0) && (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
className="mt-4 w-fit"
|
||||||
|
disabled={!canProceed}
|
||||||
|
onClick={handleProceed}
|
||||||
|
>
|
||||||
|
Proceed
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,19 @@ export function RunBlockTool({ part }: Props) {
|
|||||||
const isError =
|
const isError =
|
||||||
part.state === "output-error" ||
|
part.state === "output-error" ||
|
||||||
(!!output && isRunBlockErrorOutput(output));
|
(!!output && isRunBlockErrorOutput(output));
|
||||||
|
const setupRequirementsOutput =
|
||||||
|
part.state === "output-available" &&
|
||||||
|
output &&
|
||||||
|
isRunBlockSetupRequirementsOutput(output)
|
||||||
|
? output
|
||||||
|
: null;
|
||||||
|
|
||||||
const hasExpandableContent =
|
const hasExpandableContent =
|
||||||
part.state === "output-available" &&
|
part.state === "output-available" &&
|
||||||
!!output &&
|
!!output &&
|
||||||
|
!setupRequirementsOutput &&
|
||||||
(isRunBlockBlockOutput(output) ||
|
(isRunBlockBlockOutput(output) ||
|
||||||
isRunBlockDetailsOutput(output) ||
|
isRunBlockDetailsOutput(output) ||
|
||||||
isRunBlockSetupRequirementsOutput(output) ||
|
|
||||||
isRunBlockErrorOutput(output));
|
isRunBlockErrorOutput(output));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +64,12 @@ export function RunBlockTool({ part }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{setupRequirementsOutput && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<SetupRequirementsCard output={setupRequirementsOutput} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasExpandableContent && output && (
|
{hasExpandableContent && output && (
|
||||||
<ToolAccordion {...getAccordionMeta(output)}>
|
<ToolAccordion {...getAccordionMeta(output)}>
|
||||||
{isRunBlockBlockOutput(output) && <BlockOutputCard output={output} />}
|
{isRunBlockBlockOutput(output) && <BlockOutputCard output={output} />}
|
||||||
@@ -65,10 +78,6 @@ export function RunBlockTool({ part }: Props) {
|
|||||||
<BlockDetailsCard output={output} />
|
<BlockDetailsCard output={output} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isRunBlockSetupRequirementsOutput(output) && (
|
|
||||||
<SetupRequirementsCard output={output} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isRunBlockErrorOutput(output) && <ErrorCard output={output} />}
|
{isRunBlockErrorOutput(output) && <ErrorCard output={output} />}
|
||||||
</ToolAccordion>
|
</ToolAccordion>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,15 +6,9 @@ import { Text } from "@/components/atoms/Text/Text";
|
|||||||
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
|
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
|
||||||
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
||||||
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||||
import {
|
import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent";
|
||||||
ContentBadge,
|
|
||||||
ContentCardDescription,
|
|
||||||
ContentCardTitle,
|
|
||||||
ContentMessage,
|
|
||||||
} from "../../../../components/ToolAccordion/AccordionContent";
|
|
||||||
import {
|
import {
|
||||||
buildExpectedInputsSchema,
|
buildExpectedInputsSchema,
|
||||||
coerceCredentialFields,
|
coerceCredentialFields,
|
||||||
@@ -31,10 +25,8 @@ export function SetupRequirementsCard({ output }: Props) {
|
|||||||
const [inputCredentials, setInputCredentials] = useState<
|
const [inputCredentials, setInputCredentials] = useState<
|
||||||
Record<string, CredentialsMetaInput | undefined>
|
Record<string, CredentialsMetaInput | undefined>
|
||||||
>({});
|
>({});
|
||||||
const [hasSentCredentials, setHasSentCredentials] = useState(false);
|
|
||||||
|
|
||||||
const [showInputForm, setShowInputForm] = useState(false);
|
|
||||||
const [inputValues, setInputValues] = useState<Record<string, unknown>>({});
|
const [inputValues, setInputValues] = useState<Record<string, unknown>>({});
|
||||||
|
const [hasSent, setHasSent] = useState(false);
|
||||||
|
|
||||||
const { credentialFields, requiredCredentials } = coerceCredentialFields(
|
const { credentialFields, requiredCredentials } = coerceCredentialFields(
|
||||||
output.setup_info.user_readiness?.missing_credentials,
|
output.setup_info.user_readiness?.missing_credentials,
|
||||||
@@ -50,27 +42,49 @@ export function SetupRequirementsCard({ output }: Props) {
|
|||||||
setInputCredentials((prev) => ({ ...prev, [key]: value }));
|
setInputCredentials((prev) => ({ ...prev, [key]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const needsCredentials = credentialFields.length > 0;
|
||||||
const isAllCredentialsComplete =
|
const isAllCredentialsComplete =
|
||||||
credentialFields.length > 0 &&
|
needsCredentials &&
|
||||||
[...requiredCredentials].every((key) => !!inputCredentials[key]);
|
[...requiredCredentials].every((key) => !!inputCredentials[key]);
|
||||||
|
|
||||||
function handleProceedCredentials() {
|
const needsInputs = inputSchema !== null;
|
||||||
setHasSentCredentials(true);
|
const requiredInputNames = expectedInputs
|
||||||
onSend(
|
.filter((i) => i.required)
|
||||||
"I've configured the required credentials. Please re-run the block now.",
|
.map((i) => i.name);
|
||||||
);
|
const isAllInputsComplete =
|
||||||
}
|
needsInputs &&
|
||||||
|
requiredInputNames.every((name) => {
|
||||||
|
const v = inputValues[name];
|
||||||
|
return v !== undefined && v !== null && v !== "";
|
||||||
|
});
|
||||||
|
|
||||||
function handleRunWithInputs() {
|
const canRun =
|
||||||
const nonEmpty = Object.fromEntries(
|
!hasSent &&
|
||||||
Object.entries(inputValues).filter(
|
(!needsCredentials || isAllCredentialsComplete) &&
|
||||||
([, v]) => v !== undefined && v !== null && v !== "",
|
(!needsInputs || isAllInputsComplete);
|
||||||
),
|
|
||||||
);
|
function handleRun() {
|
||||||
onSend(
|
setHasSent(true);
|
||||||
`Run the block with these inputs: ${JSON.stringify(nonEmpty, null, 2)}`,
|
|
||||||
);
|
const parts: string[] = [];
|
||||||
setShowInputForm(false);
|
if (needsCredentials) {
|
||||||
|
parts.push("I've configured the required credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsInputs) {
|
||||||
|
const nonEmpty = Object.fromEntries(
|
||||||
|
Object.entries(inputValues).filter(
|
||||||
|
([, v]) => v !== undefined && v !== null && v !== "",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
parts.push(
|
||||||
|
`Run the block with these inputs: ${JSON.stringify(nonEmpty, null, 2)}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
parts.push("Please re-run the block now.");
|
||||||
|
}
|
||||||
|
|
||||||
|
onSend(parts.join(" "));
|
||||||
setInputValues({});
|
setInputValues({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,119 +92,54 @@ export function SetupRequirementsCard({ output }: Props) {
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<ContentMessage>{output.message}</ContentMessage>
|
<ContentMessage>{output.message}</ContentMessage>
|
||||||
|
|
||||||
{credentialFields.length > 0 && (
|
{needsCredentials && (
|
||||||
<div className="rounded-2xl border bg-background p-3">
|
<div className="rounded-2xl border bg-background p-3">
|
||||||
<CredentialsGroupedView
|
<Text variant="small" className="w-fit border-b text-zinc-500">
|
||||||
credentialFields={credentialFields}
|
Block credentials
|
||||||
requiredCredentials={requiredCredentials}
|
</Text>
|
||||||
inputCredentials={inputCredentials}
|
<div className="mt-6">
|
||||||
inputValues={{}}
|
<CredentialsGroupedView
|
||||||
onCredentialChange={handleCredentialChange}
|
credentialFields={credentialFields}
|
||||||
/>
|
requiredCredentials={requiredCredentials}
|
||||||
{isAllCredentialsComplete && !hasSentCredentials && (
|
inputCredentials={inputCredentials}
|
||||||
<Button
|
inputValues={{}}
|
||||||
variant="primary"
|
onCredentialChange={handleCredentialChange}
|
||||||
size="small"
|
/>
|
||||||
className="mt-3 w-full"
|
</div>
|
||||||
onClick={handleProceedCredentials}
|
|
||||||
>
|
|
||||||
Proceed
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{inputSchema && (
|
{inputSchema && (
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="rounded-2xl border bg-background p-3 pt-4">
|
||||||
<Button
|
<Text variant="small" className="w-fit border-b text-zinc-500">
|
||||||
variant="outline"
|
Block inputs
|
||||||
size="small"
|
</Text>
|
||||||
className="w-fit"
|
<FormRenderer
|
||||||
onClick={() => setShowInputForm((prev) => !prev)}
|
jsonSchema={inputSchema}
|
||||||
>
|
className="mb-3 mt-3"
|
||||||
{showInputForm ? "Hide inputs" : "Fill in inputs"}
|
handleChange={(v) => setInputValues(v.formData ?? {})}
|
||||||
</Button>
|
uiSchema={{
|
||||||
|
"ui:submitButtonOptions": { norender: true },
|
||||||
|
}}
|
||||||
|
initialValues={inputValues}
|
||||||
|
formContext={{
|
||||||
|
showHandles: false,
|
||||||
|
size: "small",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AnimatePresence initial={false}>
|
{(needsCredentials || needsInputs) && (
|
||||||
{showInputForm && inputSchema && (
|
<Button
|
||||||
<motion.div
|
variant="primary"
|
||||||
initial={{ height: 0, opacity: 0, filter: "blur(6px)" }}
|
size="small"
|
||||||
animate={{ height: "auto", opacity: 1, filter: "blur(0px)" }}
|
className="w-fit"
|
||||||
exit={{ height: 0, opacity: 0, filter: "blur(6px)" }}
|
disabled={!canRun}
|
||||||
transition={{
|
onClick={handleRun}
|
||||||
height: { type: "spring", bounce: 0.15, duration: 0.5 },
|
>
|
||||||
opacity: { duration: 0.25 },
|
Proceed
|
||||||
filter: { duration: 0.2 },
|
</Button>
|
||||||
}}
|
|
||||||
className="overflow-hidden"
|
|
||||||
style={{ willChange: "height, opacity, filter" }}
|
|
||||||
>
|
|
||||||
<div className="rounded-2xl border bg-background p-3 pt-4">
|
|
||||||
<Text variant="body-medium">Block inputs</Text>
|
|
||||||
<FormRenderer
|
|
||||||
jsonSchema={inputSchema}
|
|
||||||
handleChange={(v) => setInputValues(v.formData ?? {})}
|
|
||||||
uiSchema={{
|
|
||||||
"ui:submitButtonOptions": { norender: true },
|
|
||||||
}}
|
|
||||||
initialValues={inputValues}
|
|
||||||
formContext={{
|
|
||||||
showHandles: false,
|
|
||||||
size: "small",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="-mt-8 flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="small"
|
|
||||||
className="w-fit"
|
|
||||||
onClick={handleRunWithInputs}
|
|
||||||
>
|
|
||||||
Run
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="small"
|
|
||||||
className="w-fit"
|
|
||||||
onClick={() => {
|
|
||||||
setShowInputForm(false);
|
|
||||||
setInputValues({});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{expectedInputs.length > 0 && !inputSchema && (
|
|
||||||
<div className="rounded-2xl border bg-background p-3">
|
|
||||||
<ContentCardTitle className="text-xs">
|
|
||||||
Expected inputs
|
|
||||||
</ContentCardTitle>
|
|
||||||
<div className="mt-2 grid gap-2">
|
|
||||||
{expectedInputs.map((input) => (
|
|
||||||
<div key={input.name} className="rounded-xl border p-2">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<ContentCardTitle className="text-xs">
|
|
||||||
{input.title}
|
|
||||||
</ContentCardTitle>
|
|
||||||
<ContentBadge>
|
|
||||||
{input.required ? "Required" : "Optional"}
|
|
||||||
</ContentBadge>
|
|
||||||
</div>
|
|
||||||
<ContentCardDescription className="mt-1">
|
|
||||||
{input.name} • {input.type}
|
|
||||||
{input.description ? ` \u2022 ${input.description}` : ""}
|
|
||||||
</ContentCardDescription>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useChatSession } from "./useChatSession";
|
import { useChatSession } from "./useChatSession";
|
||||||
import { useLongRunningToolPolling } from "./hooks/useLongRunningToolPolling";
|
import { useLongRunningToolPolling } from "./hooks/useLongRunningToolPolling";
|
||||||
|
|
||||||
const STREAM_START_TIMEOUT_MS = 30_000; // 30s to detect if backend is down/not responding
|
const STREAM_START_TIMEOUT_MS = 12_000;
|
||||||
|
|
||||||
/** Mark any in-progress tool parts as completed/errored so spinners stop. */
|
/** Mark any in-progress tool parts as completed/errored so spinners stop. */
|
||||||
function resolveInProgressTools(
|
function resolveInProgressTools(
|
||||||
@@ -203,9 +203,8 @@ export function useCopilotPage() {
|
|||||||
prevStatusRef.current = status;
|
prevStatusRef.current = status;
|
||||||
|
|
||||||
const wasActive = prev === "streaming" || prev === "submitted";
|
const wasActive = prev === "streaming" || prev === "submitted";
|
||||||
const isReady = status === "ready";
|
const isIdle = status === "ready" || status === "error";
|
||||||
// Only invalidate on successful completion, not on error to avoid infinite refetch loop
|
if (wasActive && isIdle && sessionId) {
|
||||||
if (wasActive && isReady && sessionId) {
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: getGetV2GetSessionQueryKey(sessionId),
|
queryKey: getGetV2GetSessionQueryKey(sessionId),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function CredentialsFlatView({
|
|||||||
) : (
|
) : (
|
||||||
!readOnly && (
|
!readOnly && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={onAddCredential}
|
onClick={onAddCredential}
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { RJSFSchema } from "@rjsf/utils";
|
import { RJSFSchema } from "@rjsf/utils";
|
||||||
import { preprocessInputSchema } from "./utils/input-schema-pre-processor";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { customValidator } from "./utils/custom-validator";
|
|
||||||
import Form from "./registry";
|
import Form from "./registry";
|
||||||
import { ExtendedFormContextType } from "./types";
|
import { ExtendedFormContextType } from "./types";
|
||||||
|
import { customValidator } from "./utils/custom-validator";
|
||||||
import { generateUiSchemaForCustomFields } from "./utils/generate-ui-schema";
|
import { generateUiSchemaForCustomFields } from "./utils/generate-ui-schema";
|
||||||
|
import { preprocessInputSchema } from "./utils/input-schema-pre-processor";
|
||||||
|
|
||||||
type FormRendererProps = {
|
type FormRendererProps = {
|
||||||
jsonSchema: RJSFSchema;
|
jsonSchema: RJSFSchema;
|
||||||
@@ -12,15 +13,17 @@ type FormRendererProps = {
|
|||||||
uiSchema: any;
|
uiSchema: any;
|
||||||
initialValues: any;
|
initialValues: any;
|
||||||
formContext: ExtendedFormContextType;
|
formContext: ExtendedFormContextType;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FormRenderer = ({
|
export function FormRenderer({
|
||||||
jsonSchema,
|
jsonSchema,
|
||||||
handleChange,
|
handleChange,
|
||||||
uiSchema,
|
uiSchema,
|
||||||
initialValues,
|
initialValues,
|
||||||
formContext,
|
formContext,
|
||||||
}: FormRendererProps) => {
|
className,
|
||||||
|
}: FormRendererProps) {
|
||||||
const preprocessedSchema = useMemo(() => {
|
const preprocessedSchema = useMemo(() => {
|
||||||
return preprocessInputSchema(jsonSchema);
|
return preprocessInputSchema(jsonSchema);
|
||||||
}, [jsonSchema]);
|
}, [jsonSchema]);
|
||||||
@@ -31,7 +34,10 @@ export const FormRenderer = ({
|
|||||||
}, [preprocessedSchema, uiSchema]);
|
}, [preprocessedSchema, uiSchema]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"mb-6 mt-4"} data-tutorial-id="input-handles">
|
<div
|
||||||
|
className={cn("mb-6 mt-4", className)}
|
||||||
|
data-tutorial-id="input-handles"
|
||||||
|
>
|
||||||
<Form
|
<Form
|
||||||
formContext={formContext}
|
formContext={formContext}
|
||||||
idPrefix="agpt"
|
idPrefix="agpt"
|
||||||
@@ -45,4 +51,4 @@ export const FormRenderer = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -218,6 +218,17 @@ If you initially installed Docker with Hyper-V, you **don’t need to reinstall*
|
|||||||
|
|
||||||
For more details, refer to [Docker's official documentation](https://docs.docker.com/desktop/windows/wsl/).
|
For more details, refer to [Docker's official documentation](https://docs.docker.com/desktop/windows/wsl/).
|
||||||
|
|
||||||
|
### ⚠️ Podman Not Supported
|
||||||
|
|
||||||
|
AutoGPT requires **Docker** (Docker Desktop or Docker Engine). **Podman and podman-compose are not supported** and may cause path resolution issues, particularly on Windows.
|
||||||
|
|
||||||
|
If you see errors like:
|
||||||
|
```text
|
||||||
|
Error: the specified Containerfile or Dockerfile does not exist, ..\..\autogpt_platform\backend\Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
This indicates you're using Podman instead of Docker. Please install [Docker Desktop](https://docs.docker.com/desktop/) and use `docker compose` instead of `podman-compose`.
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user