Commit Graph

1754 Commits

Author SHA1 Message Date
Abhinav Raut 1034617b63 point livechat verification email replies at a no-reply address
Livechat customers verify by pasting the code into chat, not by replying to the email. If the notification sender happens to be the same mailbox as a polled inbox, a stray email reply would get ingested as a new conversation. Setting Reply-To to noreply@<sender-domain> keeps those replies out of any monitored inbox. The email conversation path is unchanged and stays replyable.
2026-07-20 23:04:41 +05:30
Abhinav Raut 7760974e07 send AI verification code through the conversation's inbox
The OTP email went out over the notification email channel, which uses a separate SMTP config and a different sender. Now email conversations send the code through their own inbox, reusing the conversation subject so it threads into the same email thread instead of a new one. Livechat still falls back to the notification channel since it can't send email.
2026-07-20 21:30:25 +05:30
Abhinav Raut 085880d296 make OTP attempt check atomic to prevent brute-force bypass 2026-07-20 18:12:48 +05:30
Abhinav Raut bfc3b6e196 gate AI custom tools behind email OTP verification
Custom tools can now require a verified contact before they run. This gives tools a trustworthy signal about who the customer is without relying on DMARC or the JWT login.

ai_tools gets a requires_verification column, defaulting to true (fail-closed). A flagged tool is blocked in httpTool.Execute until the conversation is verified, and every tool call now carries an X-Libredesk-Contact-Verified header so tool authors can tell an OTP-verified contact from a self-claimed one.

The AI agent gets three native tools: send_email_verification emails a 6-digit code out of band, check_email_verification confirms it, and set_contact_email lets an anonymous visitor add an email to send the code to. Codes and the verified window live in Redis, scoped per conversation, with attempt and resend caps. JWT livechat contacts stay trusted without OTP; email contacts and visitors verify by code.

The tool admin form gets a "require verified contact" toggle (default on) with a confirm dialog when turning it off.
2026-07-20 17:48:41 +05:30
Abhinav Raut 752e6042e2 drop CC'd contacts' messages from the AI agent history
The AI agent's tools act as the primary contact. So any message that entered the history as a trusted user turn could inject instructions that drive those tools under the contact's identity. A CC'd participant on the conversation was one such source.

buildHistory now takes the primary contact's ID. It keeps the contact's own messages and the agent's replies, and drops messages from any other contact before building the prompt.
2026-07-20 00:09:00 +05:30
Abhinav Raut 6015859574 scope copilot thinking state per conversation
isThinking was a single global ref, so switching conversations while a copilot
send was still in flight showed the thinking indicator in the wrong conversation
and blocked sending there until the other request settled. Key it by uuid like
the revision and message state already are.
2026-07-19 23:57:01 +05:30
Abhinav Raut 1384fd5d42 scope Generate Reply tools to the conversation's contact
Copilot and Generate Reply shared one tool builder, so Generate Reply could reach any contact's conversations through the unscoped search tools. That is too broad for a draft that only concerns the current customer.

Split it into two builders. copilotTools keeps the full cross-contact search set. generateReplyTools only exposes fetch_conversation and list_contact_conversations, both scoped to the current contact, and returns no tools when there is no contact so it fails closed.

fetchConversationTool now takes a contactID and returns the same not-found message when a fetched conversation belongs to a different contact. The reply prompt only mentions the history tools when they are actually attached.
2026-07-19 23:46:19 +05:30
Abhinav Raut f460f6b163 expand copilot with customer-history tools, persona picker, and tag suggestions
Copilot and Generate Reply get four read-only, access-filtered tools to look up
a customer's history: list the contact's other conversations, search
conversations by exact email, fetch one conversation by reference number, and
search contacts. Tool output is marked untrusted and capped so a big response
can't blow the context window.

Copilot panel changes:
- per-agent persona picker that borrows an enabled assistant's voice, language
  and instructions without changing the tool set (stored in localStorage)
- answers render as HTML; copy, insert-into-reply, and add-as-private-note
  actions per answer
- chat history moves to its own copilot store; server history is persisted only
  after a successful reply and read back with a limit

Adds AI tag suggestions: a new endpoint suggests up to 3 existing tags for a
conversation, applied from the sidebar, never auto-applied.

Hardening and fixes:
- OIDC callback rejects non-agent users
- custom tool URLs and params schema validated on save; tool HTTP client no
  longer follows redirects; query keys pinned in the tool URL win
- GetAllConversationMessages takes an explicit limit (cap 1000)
- FAQ mining skips a candidate already pending review
- ai agent handoff records its event only after the move actually lands
- share chat message role constants; rename v2.7.0 migration to v2.6.0 and add
  the fix_grammar_spelling prompt
2026-07-19 23:33:18 +05:30
Abhinav Raut dd3465f6bc make the quoted text toggle keyboard accessible 2026-07-19 18:58:36 +05:30
Abhinav Raut b570502a20 remember when a provider needs max_completion_tokens and track token usage
Some OpenAI-compatible endpoints reject max_tokens and want
max_completion_tokens. Before, every request paid a 400 round trip to
learn this. Now we cache the swap per base URL and model, so once one
request adapts, later requests send the right param up front. Only adapt
when the error code is unsupported_parameter, not on any max_tokens error.

Parse the usage block from provider responses into a TokenUsage struct and
log prompt/completion/total tokens. Added debug logging of model content,
tool results, and RAG chunk text to make agent runs easier to trace.

Widget side: clean up chat bubble spacing by moving margins off the text
and onto trailing elements, and drop the bottom margin on the last
paragraph/list in rendered HTML so bubbles don't have extra padding.
2026-07-19 18:47:36 +05:30
Abhinav Raut ec5333a393 move OIDC client secret masking to the backend
The frontend used to blank out the masked secret before saving. Now it sends the value as-is and the backend keeps the stored secret when it sees the dummy mask. This matches how webhooks and other secrets already work.
2026-07-19 12:35:07 +05:30
Abhinav Raut 52cebdf384 update livechat launcher logo 2026-07-19 10:57:23 +05:30
Abhinav Raut b1d6fb4e00 limit when the AI agent appends the confirm prompt
Only append [[confirm]] after a real question is fully answered. Skip it for greetings, small talk, clarifying questions, refusals, and partial answers, or when the customer already signaled they are done.
2026-07-19 10:48:31 +05:30
Abhinav Raut 35d69fc1c3 fix review findings in reports overview and avatar upload
Guard avatar edits while a save is in flight. The AvatarUpload now takes
a disabled prop that blocks the file picker and the remove button, and
the profile view ignores crop/remove events while saving. This stops a
second crop or a remove from racing an ongoing update.

Only clear the pending file after the delete request succeeds, so a
failed remove keeps the cropped preview and the Save button usable.

Do not mark failed report refreshes as fresh. The fetch helpers now
report success, and lastUpdate only advances when every section loads.
A fully failed refresh no longer labels stale data as just updated.

Add an aria-label to the custom range days input so assistive tech can
name it.
2026-07-18 19:55:59 +05:30
Abhinav Raut 94334a02c6 polish reports overview, avatar upload, and conversation list UI
Reports overview now has a single range picker (7/30/90 days plus a
custom input) that drives every card at once, instead of a separate
date dropdown per card. Auto-refresh polls every 5 minutes, pauses
when the tab is hidden, and stops after an hour until you refresh.
Card titles are muted, numbers use tabular figures, and SLA colors use
theme tokens instead of raw green/red. The old DateFilter component is
gone.

Move avatar cropping into the shared AvatarUpload component and reuse
it on the profile page. Save is disabled until a new image is picked.

Conversation list emphasizes unread rows with bolder text and tighter
spacing. Empty state, MenuCard, and chart legend get small style
fixes. Consolidate several one-off i18n strings into shared keys.
2026-07-18 19:42:42 +05:30
Abhinav Raut 8ac465c51a support multiple auth headers on AI tools and tune agent timeouts
Tool auth used to be a single header/value pair. It is now a list of
headers, each value encrypted at rest and masked in the API. Editing a
tool keeps an untouched (masked) value's existing secret, matched by
header name. Covered with unit tests.

Agent tuning: a full agent run is now capped (90s for livechat since the
customer is watching, 3min for email), the per-provider-call timeout
drops 90s to 60s with 2 retries, and default worker_count goes 2 to 10
for burst headroom. Idle workers are free; the run cap is what protects
the pool.

Also adds help text and placeholders across the AI admin pages.
2026-07-18 18:11:10 +05:30
Abhinav Raut e7fa4cedad fix review findings in the aiagent package
Guard the handoff and resolve paths against double handoffs, swallowed reply errors, and a stale team snapshot. Also fail closed on turn-count errors, spend the image budget newest-first, fail boot on an empty assistant set, and keep deleted assistants recognized as AI so FAQ mining never treats their replies as human.
2026-07-18 12:35:43 +05:30
Abhinav Raut a4be8ecb95 fix review findings in the ai package
Correctness fixes from the internal/ai code review:
- gate reindex commits on the snippet row still existing, so a delete
  racing reconcile can't re-insert its embeddings forever
- cap one logical provider request at 90s across all retries, so a
  hanging provider can't stall the reply box for minutes
- return proper error envelopes from the copilot message store instead
  of raw sqlx errors that surfaced as "Error interface conversion failed"
- reject blank AI replies in the generate-reply and copilot handlers
- build GET tool URLs with url.Parse so a # in the URL doesn't swallow
  the query params
- let a blank api_key clear the stored key (masked keeps it), restoring
  a way to disable AI

Cleanups and hardening:
- share one SSRF transport between tool and provider clients instead of
  building a new transport per AI call
- copy the provider config struct instead of listing every field
- backfill temperature 0.7 for configured providers upgrading from
  releases that hardcoded it
- only pass user/assistant roles from copilot history to the provider
- reject enc:-prefixed secrets that would be stored raw and fail decrypt
- rune-safe truncation of test errors, one-line doc comments
2026-07-18 12:16:33 +05:30
Abhinav Raut fe17af207b drop hardcoded transport fallback in ssrf.NewTransport
The DefaultTransport type-assert guard hardcoded the stdlib pool defaults, the exact duplication cloning was meant to avoid, to defend against a case that never happens in this codebase. Back to the plain clone.
2026-07-18 11:49:26 +05:30
Abhinav Raut 8f34fc5ee5 guard against a replaced http.DefaultTransport in ssrf.NewTransport
Type-asserting http.DefaultTransport.(*http.Transport) panics if a dependency swaps the global for a different RoundTripper. Fall back to a fresh transport with the standard defaults so boot can't crash.
2026-07-18 11:40:12 +05:30
Abhinav Raut c7f55e2394 guard reply-box AI prompt race and share the SSRF-safe http transport
handleAiPromptSelected now captures the conversation uuid and bails if it changed while the completion was in flight, and uses convertTextToHtml with a null guard, matching handleGenerateReply. This stops a generated prompt from landing in the wrong conversation's draft or crashing on an empty response.

Add ssrf.NewTransport that clones http.DefaultTransport (keeping proxy and connection-pool defaults) and applies the dial guard, then use it for the OIDC, AI, OpenAI, and webhook clients. The bare transports were dropping HTTP_PROXY handling and pool defaults.
2026-07-18 11:20:16 +05:30
Abhinav Raut a3cdd7111e remove inline OpenAI key prompt and reuse generating state for AI prompts
The reply-box AI prompts used a legacy inline dialog to set the OpenAI key, hitting a separate PUT /api/v1/ai/provider endpoint. That is now fully covered by the admin provider settings page, so drop the dialog, the endpoint, the UpdateProvider method, the set-completion-key query, and the orphaned i18n keys. With no provider configured an agent now gets the 'ask your administrator' toast instead.

Also make the inline AI prompts (Make Friendly, etc.) toggle the existing isGenerating state so the editor shows the same generating animation as Generate Reply, with a guard against overlapping runs.
2026-07-18 03:13:31 +05:30
Abhinav Raut e5ce8e3358 truncate long assistant descriptions in admin table 2026-07-18 02:44:19 +05:30
Abhinav Raut 0f2aef1c51 fix Groq and Together AI provider preset defaults 2026-07-18 02:15:50 +05:30
Abhinav Raut 97c227b6f8 skip AI agent on non-open status categories instead of hardcoded names 2026-07-18 01:38:30 +05:30
Abhinav Raut 758a01ab74 guard knowledge base URL import against SSRF and fix OIDC transport defaults
The earlier SSRF work covered the provider, custom-tool, webhook, and OIDC
outbound calls but missed the knowledge base URL import path, which still used
http.DefaultClient. An admin importing a URL could reach loopback, link-local,
or RFC1918 hosts. Renamed the Manager's toolClient to httpClient (it is a
general SSRF-guarded outbound client now) and pointed fetchURL at it. The guard
sits on the dialer's Control, so it also runs on redirect targets, not just the
first hop.

Also fixed the OIDC discovery client. Its hand-built transport dropped two
defaults that http.DefaultTransport sets. Added Proxy: http.ProxyFromEnvironment
so it honors HTTP_PROXY/HTTPS_PROXY (SSO discovery now works behind an egress
proxy) and ForceAttemptHTTP2 so HTTP/2 is negotiated.
2026-07-17 19:24:31 +05:30
Abhinav Raut eed50ef602 link SSRF docs page from SECURITY.md 2026-07-16 14:52:03 +05:30
Abhinav Raut 9566dcbf4d apply SSRF guard to all admin-configured outbound URLs
Move the webhook SSRF guard into a shared internal/ssrf package and wire it
into every place the server fetches an admin-set URL: webhooks, OIDC discovery,
the AI provider base URL, and custom AI tool calls. Add a global [ssrf] config
block, off by default with an allowed_cidrs bypass, so single-tenant self-hosters
keep reaching internal hosts while multi-tenant or hosted deploys can turn it on.
The old [webhook] allowed_hosts key is still read for backward compat and folds
into the guard.
2026-07-16 12:58:12 +05:30
Abhinav Raut d02b65f2bd address CodeRabbit findings in AI embedding and assistant flows
Fix a reindex race: snippet embedding runs outside the lock, so a slower
job from an older edit could commit stale vectors after a newer edit.
Split Reindex into embed (lock-free) and commit (locked), and gate the
commit with a per-snippet generation counter so only the latest edit
wins. Delete drops the counter so an in-flight job can't re-insert
vectors for a deleted snippet.

Clear the DB avatar reference before deleting the avatar media file. The
old order deleted the file first, so a failed DB update left the DB
pointing at a missing file and a broken image.

Also handle unchecked errors flagged by errcheck (tx.Rollback,
html.Render, fmt.Fprintf), guard capToTokens against a negative limit
that would panic, and stop logging the full import URL since it can
carry credentials.
2026-07-16 12:50:02 +05:30
Abhinav Raut c030c9ee4b harden AI embedding and agent prompt handling, add tokenizer, and switch to a green theme
Embedding:
- Persist embedding_max_tokens. It was dropped when saving the provider config
  and always reset to the 8192 default, so self-hosted models with a smaller
  limit kept getting rejected.
- Split embedding requests to stay under the provider's item and token caps. A
  large snippet or URL import used to go in one request, hit the cap, fail, and
  get retried every minute forever.
- Skip blank chunks. An empty input string makes the API reject the whole batch.
- Add the tiktoken tokenizer (cl100k_base) with tests for token counting.

Agent prompt:
- Move contact fields, the conversation subject, and custom attributes out of
  the system prompt and into a delimited user-role block, so a crafted name or
  subject can't act as an instruction.

Frontend:
- Switch the primary and sidebar colors to a green theme.
- Show a "summarizing" info toast while the AI summary runs, and add an info
  toast variant.
- Make the snippet content box taller and the snippet dialog wider.
- Drop a hardcoded dark hover color on the scroll-to-bottom button.
2026-07-16 11:20:38 +05:30
Abhinav Raut 8cdfe861e8 add AI snippet URL import, conversation summarize, and assistant reply languages
Snippet import: new "Import from URL" flow fetches a page and stores its
readable content as a snippet. Extraction uses the mackee/go-readability
library (Mozilla Readability port) and outputs Markdown, so nav/footer
boilerplate is dropped. The snippet list shows the source, and the edit
dialog is now wider with a taller content box.

Summarize: new "Summarize with AI" action on a conversation calls the AI and
adds the result as a private note. It shows an info toast right away so the
user knows it started, since the call can take a few seconds. This adds an
"info" toast variant that any feature can use.

Assistant languages: assistants can be given a list of allowed reply
languages. The assistant replies in the customer's language when it is one of
them, otherwise it falls back to the first. The preview also lists the
knowledge sources it used.

Cleanup: replace hardcoded gray/zinc/white colors with theme tokens
(text-muted-foreground, text-foreground, bg-accent) across several components.
2026-07-16 01:39:45 +05:30
Abhinav Raut afb338cb54 address PR review findings in the AI agent
Thread context through provider calls so cancelled requests stop retrying, make snippet delete and FAQ review transitions atomic, cap provider response reads, guard stale AI replies and copilot responses from overwriting newer conversation state, and stop logging raw search queries and chunk content.
2026-07-14 01:17:47 +05:30
Abhinav Raut 976c45df3e add AI provider connection test and self-heal reasoning model param errors
Reasoning models like gpt-5 reject max_tokens and non-default temperature.
The client now reads the structured 400, renames max_tokens to
max_completion_tokens or drops the bad tuning param, and retries. This
removes the old rule that only sent max_completion_tokens when reasoning
effort was set.

Provider form gets a "Test connection" button that makes one live call
with the form values and shows the provider's real error. Embedding test
also checks the returned vector length against the Dimensions field.
Add an Anthropic preset (their OpenAI-compatible endpoint) and stop
pre-filling temperature since blank is safe on every model.

Fix the shared Button loader so the spinner is visible on non-filled
variants, and polish the copilot panel: bot avatars on messages, dot
loader while thinking, cleaner input box, and tab slide-in animation.
2026-07-13 03:01:10 +05:30
Abhinav Raut e1695b7836 send AI agent confirmation question as a separate widget message
The model marks its trailing confirmation question with a [[confirm]] line.
We split that off and send it as its own chat bubble so the widget reads like
a real conversation. The customer never sees the marker. Email is left as one
combined message since separate bubbles only suit the widget.

Confirmation messages are tagged with is_confirmation in meta so they no
longer inflate the reply count in assistant stats.

Keep the widget typing indicator alive during long agent runs by
re-broadcasting every 3s, under the widget's 5s typing expiry.

Fix the AI assistant and tool edit forms to stay on the page after save.
They now only go back to the list when creating, matching the rest of the
admin forms.
2026-07-13 02:23:35 +05:30
Abhinav Raut 85a38699da restrict copilot to built-in tools and add per-assistant handoff toggle
Custom tools are HTTP calls with no read-only guarantee, so a mutating tool
could fire from copilot chat or while drafting a reply. Copilot and
generate-reply now get only the built-in knowledge search, and custom tools
stay exclusive to assistants where admins pick them explicitly. This matches
Intercom, Chatwoot, and Freshdesk. The contact identity headers now flow only
on the assistant path.

Also:
- new "Offer handoff to a human" switch on assistants (default on). When off,
  the hand_off_to_human tool is not registered and the prompt tells the
  assistant to say it cannot help instead of offering a human. Safety exits
  (error, max turns, other participant) still unassign as before.
- workspace admin instructions from the AI config no longer leak into the
  customer-facing assistant prompt.
- copilot and reply-draft prompts now treat conversation text and tool
  outputs as untrusted data.
2026-07-13 01:57:44 +05:30
Abhinav Raut dad99bf0ee render AI agent replies as markdown and fix assistant identity, handoff, and stats bugs
Replies from the AI agent are now converted from markdown to HTML with
goldmark before queueing, so bold, links, and lists render properly in the
widget, agent app, and email. The prompt now allows simple markdown. Raw
HTML in model output is escaped by goldmark, and both frontends sanitize
on render anyway.

Other fixes bundled in:
- validate avatar type and size before creating or updating an assistant,
  and roll back the assistant if the avatar upload fails after create
- return 404 from agent update and API key endpoints for AI assistant
  identity users, and hide assistants from mention and SLA user pickers
- reserve the autonomous assistant's built-in tool names so custom tools
  cannot shadow them
- apply resolve after the reply is posted so the CSAT survey follows the
  answer instead of preceding it
- unassign the assistant on handoff even when the fallback team is the
  same team
- count reopens by status category instead of status name, and exclude
  CSAT messages from the turn cap
- split oversized wrapper divs into child blocks when chunking KB HTML
  instead of truncating them
- return 404 when soft-deleting an already-deleted agent, and keep AI
  assistants (which have no email) visible in the compact users list
2026-07-12 23:12:44 +05:30
Abhinav Raut 3137a3ff9b add conversation attributes to AI agent prompt and mark customer data as untrusted 2026-07-12 20:47:15 +05:30
Abhinav Raut 34241f7d67 improve AI agent context and disclosure
Pass the conversation subject to the AI agent, show the AI's own expectation in the chat widget while it handles a chat, and label AI messages in continuity emails.
2026-07-12 16:34:36 +05:30
Abhinav Raut a7288e11b0 add AI assistant expectation message shown in the livechat widget 2026-07-12 13:29:59 +05:30
Abhinav Raut 44925e69a7 fix AI agent reply, turn-cap, and stats bugs
Stop dropping customer follow-ups sent mid-response, reset the turn cap only on reassignment, send max_tokens for non-reasoning models, and make FAQ approval atomic. Also exclude CSAT surveys from assistant stats and remove unused scoped-search code.
2026-07-12 12:15:32 +05:30
Abhinav Raut 76d16285ab gate AI tool identity to verified contacts and merge v2.6.0 migration into v2.7.0 2026-07-12 05:10:45 +05:30
Abhinav Raut 3ba2b8281e Merge remote-tracking branch 'origin/main' into feat/ai-agent 2026-07-12 04:50:24 +05:30
Abhinav Raut 51d29e0e0e add local widget JWT test page
A single-file page to test the livechat widget's JWT contact auth locally. Signs an HS256 token in the browser from an editable payload and loads the widget with it.
2026-07-12 04:48:39 +05:30
Abhinav Raut 623fa2c478 load agents directly in AgentList
Fetch agents through the API in AgentList instead of the shared users store, so the list does not depend on store state populated elsewhere.
2026-07-12 04:48:39 +05:30
Abhinav Raut c8884ec005 add AI reply-box UI and AI author links
Show an animated border on the reply box while a reply is generating, and hide generate-reply for private notes. Link an AI assistant message author to the assistants settings page for admins who can manage AI.
2026-07-12 04:48:39 +05:30
Abhinav Raut ac403791fd improve copilot reply drafts and add presets
Reply drafts are now written in first person as the agent and never offer to escalate, since a human is already handling the conversation. Add quick preset buttons to the copilot panel and reuse a shared transcript helper.
2026-07-12 04:48:29 +05:30
Abhinav Raut ef0add0fff add reasoning effort setting for providers
Add a reasoning effort field to the completion provider config, sent as-is to the model. Reasoning models such as GPT-5.x need it set to "none" to use tools. Also switch to max_completion_tokens.
2026-07-12 04:48:01 +05:30
Abhinav Raut 1343790353 restrict custom tool methods to GET and POST
Limit custom tools to GET and POST and validate the method on save. GET args now go on the query string and POST args in the body. Also stop sending the raw stored secret when its decryption fails, which would leak ciphertext and fail auth confusingly.
2026-07-12 04:47:50 +05:30
Abhinav Raut 0d755fc777 send CSAT on every resolve path
CSAT was only sent when a human resolved from the UI. AI and automation resolves go straight through UpdateConversationStatus and skipped it. Moved the send there so all paths trigger it. It is idempotent and no-ops when the contact has no email.
2026-07-12 04:47:43 +05:30
Abhinav Raut 16c79a2706 guard AI agent replies and add prior-conversation context
The agent now only replies to a turn the primary contact authored, so a CC'd or plus-address participant hands off to a human instead of driving tool calls under the contact's identity. It also asks the customer to confirm before resolving, and can read the contact's other recent conversations for context.
2026-07-12 04:47:36 +05:30