- CI now runs pnpm install and includes node_modules in the zip artifact
- startup.sh simplified: removed pnpm install fallback, uses direct bin path instead of npx
- Azure Web App always-on enabled to prevent cold starts
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- startup.sh now reads ${PORT:-8080} so Azure App Service can inject
the correct port via its PORT environment variable
- Removed obsolete deploy-backend.yml and deploy-frontend.yml workflows
(old Python backend and Next.js frontend pipelines)
- Removed WEBSITES_PORT=2024 from soc-langgraph Azure config
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Oryx runs `npm install` which fails on peer dependency conflicts between
@assistant-ui/react@^0.8.0 and @assistant-ui/react-syntax-highlighter@^0.7.2.
Adding .npmrc with legacy-peer-deps=true and CUSTOM_BUILD_COMMAND for pnpm.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create 5 enterprise UI card components under src/agent-uis/enterprise/:
knowledge-result, ticket-summary, ticket-detail, search-result, sandbox-result
- Update ComponentMap in src/agent-uis/index.tsx to replace () => null placeholders
with real component imports
- Implement src/main.tsx with useStream + LoadExternalComponent for full Chat UI,
connecting to LangGraph agent with VITE_LANGGRAPH_URL env var
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
workspace/ files existed on disk but were not included in previous
incremental commit, causing git to record them as deleted. Re-adding
all workspace card components, AgentWorkspace, ActivityTimeline, and
WorkspaceCardRenderer to properly track them.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
## Fixes
- [#3] Status events now add/update a status ActivityNode in timeline (upsertWsStatusNode)
- [#4] Done callback adds done node; error callback adds error node + sets session.status="error"
- [#5] handleRegenerate fully synced with workspace: status/tool/card/done/error all handled
- [#1][#2] localStorage persistence: completed/error sessions auto-saved, lazy-loaded on demand
- [#1] handleSelectConversation preloads workspace sessions from localStorage for history messages
- Refactored completeWsSession to include done ActivityNode in timeline
- Added errorWsSession, upsertWsStatusNode, saveWsToStorage, loadWsFromStorage helpers
## Known limitation
- [#7] workspace_card merge:true not yet used by backend (all cards are append-only for now)
- History workspace recovery depends on localStorage (browser-local, not cross-device)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Two bugs caused the same AI response to appear twice:
1. handleSelectConversation would fetch conversation history even while
a stream was in progress (isLoading). If the fetch resolved mid-stream,
fetchConversation overwrote the conversation messages unconditionally,
writing the server-persisted AI message (with a real UUID) alongside the
still-streaming client-side message (with a temp ID), producing a duplicate.
Fix: guard the fetch with !isLoading, and add a second check inside the
.then callback so setConversations only writes history if the messages
array is still empty (protects against slow-resolving network requests).
2. onDone in streamChat could theoretically be called a second time if the
ReadableStream completed without a "done" SSE event. Fix: call
reader.cancel() immediately after onDone so the while loop exits cleanly.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add _extract_output_str() to unwrap MCP ToolMessage content objects
instead of calling str() on the raw object
- Handle read_url url param when LLM passes a list instead of a string
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add langchain-community dependency for GoogleSerperAPIWrapper
- Add serper_api_key to config with default key
- Create app/tools/serper.py with async serper_search tool
- Register "serper" key in tools/__init__.py (independent from "search"/Jina MCP)
- Add tool title and input/output summaries in chat.py
- Set SERPER_API_KEY on Azure App Service (Operation resource group)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add has_tool_activity flag to distinguish first agent pass (thinking)
from post-tool agent pass (generating)
- Replace on_chain_start debug logging with status SSE emission
- Filter to only graph-level agent nodes via graph:step: tag prefix
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- tool_start/tool_end/tool_error SSE payloads now include call_id field
derived from LangGraph run_id for reliable tool event correlation
- tool_start_ts dict keyed by call_id instead of tool_name to handle
concurrent calls to the same tool
- Added on_chain_start debug logging to observe chain names and metadata
(no SSE emission yet, observation only)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add CoT execution trace to the SSE stream: status events for stage
transitions, enriched tool_start with input_summary, tool_end with
output_summary and duration_ms, and tool_error for failed tool calls.
Helper functions _sse, _summarize_input, _summarize_output provide
human-readable summaries for each tool type.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Replace the three pill buttons (Auto/Flash/Pro) in GeminiInput with a
single shadcn/ui Select component. Selection logic and onSelectedModelChange
callback are unchanged; only the UI widget is swapped.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
When the frontend passes a conversation_id that does not exist in the
conversations table (e.g. new conversation before first message, or
invalid ID), the INSERT into attachments failed with
ForeignKeyViolationError.
Now upload_attachment checks session.get(Conversation, conversation_id)
before inserting. If the conversation does not exist, conversation_id is
set to None and the attachment is stored as unlinked (blob path uses
_unlinked/ prefix). This avoids the FK constraint error while keeping
the file safely uploaded.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
When a tool call crashes (e.g. httpx timeout in kb_search), the LangGraph
checkpoint retains an AIMessage with tool_calls but no corresponding
ToolMessage. Subsequent requests to the same conversation_id fail with:
ValueError: Found AIMessages with tool_calls that do not have a
corresponding ToolMessage
Now the except block in _stream_response detects this specific ValueError
by checking for "tool_calls" and "ToolMessage" in the error string, then
calls checkpointer.adelete_thread() to purge the corrupted thread state.
The frontend receives {"type":"error","content":"对话状态异常,已自动重置..."}
followed by {"type":"done"}, so the user can simply resend their message.
API confirmed: AsyncPostgresSaver.adelete_thread(thread_id) deletes from
checkpoints, checkpoint_blobs, and checkpoint_writes tables for the thread.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
KB Agent runs on Azure App Service which cold-starts after idle periods,
taking 20-30s to respond. The previous 15s timeout caused httpx.ReadTimeout
that crashed the ReAct agent tool node and killed the SSE stream.
Changes:
- Increase kb_agent_search_timeout_sec default from 15 to 30 in config.py
- Add single retry with 2s backoff on ReadTimeout in kb_search tool
- Catch all exceptions and return friendly Chinese error messages instead
of propagating exceptions to the agent loop
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove menu close from onMouseLeave (was closing before user reaches dropdown)
- Add global document click listener to close menu when clicking outside
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Bug 1 - Sidebar MoreHorizontal now opens a dropdown menu:
- Click three-dot icon to toggle rename/delete dropdown
- Rename: inline text input with Enter to confirm, Escape to cancel
- Delete: removes from list, calls DELETE /api/conversations/{id}
- Rename calls PATCH /api/conversations/{id} with new title
- Both use optimistic local state updates
Bug 2 - File upload chip buttons now respond to clicks:
- Added e.stopPropagation() to X (remove) and retry buttons
- Prevents click from being swallowed by parent elements
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add return_dto=None to delete_attachment decorator to fix Litestar
ImproperlyConfiguredException on 204 status code with None return type
- Catch BaseException instead of Exception in create_tables() to handle
anyio ExceptionGroup from concurrent gunicorn workers
- Enable debug=True temporarily to capture detailed error traces
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
When 2 gunicorn workers start simultaneously, both call create_all()
which races on CREATE TABLE attachments. The second worker hits a
PostgreSQL UniqueViolation on pg_type_typname_nsp_index because the
type already exists. Wrap create_tables() in try/except to handle
this gracefully.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Litestar validates that handlers with 204 status codes cannot return
a response body. The delete_attachment handler was declared as
-> Response and returned Response(content=None, status_code=204),
which caused ImproperlyConfiguredException at startup and crashed
all workers. Change return type to None and move status_code to the
@delete decorator.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change FormData field name from "file" to "data" (backend expects "data")
- Pass conversation_id as query parameter instead of form field
- Add content_type and created_at to AttachmentData interface to match
backend response shape
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- lib/api.ts: add AttachmentData interface, uploadAttachment() function,
and getAttachmentDownloadUrl() helper for upcoming /api/attachments endpoint
- GeminiInput.tsx: wire + button to trigger file selection with upload
state management (uploading/done/error), file preview chips above
textarea with retry and remove controls, pass AttachmentData to parent
on submit
- GeminiMessage.tsx: add attachments field to Message interface, render
attachment list (filename + size + download link) below both user and
assistant message content
- GeminiChat.tsx: accept AttachmentData[] in handleSend, store attachments
on user messages for display
Backend /api/attachments endpoints not yet live — frontend is ready for
integration once backend agent completes the upload/download API.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add Attachment ORM model (id, conversation_id, message_id, filename, content_type, blob_url, size_bytes, created_at) with FK to conversations
- Add POST /api/attachments/upload (multipart, 50MB limit), GET /api/attachments/{id}, GET /api/attachments/{id}/download (302 to SAS URL), DELETE /api/attachments/{id}
- Add delete_blob() and generate_sas_url() to storage/blob.py for download redirect and cleanup
- Dispatch parse_attachment task to Service Bus for parseable types (PDF, images, CSV, DOCX, XLSX)
- Pre-create blob container on startup (best-effort)
- Add AttachmentOut schema and full API docs in doc/api.md
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove selected-tool chip/tag area above the input textarea; tool
selection state is now shown only via button highlight in the tools panel
- Add "Auto" as a third model option between Flash and Pro (default)
- Auto maps to "flash" when sent to the backend (which only accepts
flash|pro)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>