685 Commits

Author SHA1 Message Date
Abhinav Raut 189c0860e1 Merge pull request #455 from abhinavxd/chore/tidy-migration-comments
remove redundant comments in v2.6.0 migration
2026-07-31 10:20:57 +05:30
Abhinav Raut 232d445442 Merge pull request #454 from amaanJvd/fix/unsnoozer-dropped-error-log
fix(log): don't drop the error detail in unsnoozeAll log call
2026-07-31 10:20:28 +05:30
Abhinav Raut 081cc46aa2 remove redundant comments in v2.6.0 migration 2026-07-31 10:19:51 +05:30
amaanJvd 3d5d67b554 fix(log): don't drop the error in unsnoozeAll log call
The error was passed as a positional argument without a key. logf drops
the last field when the field count is odd (handleLog: "If there are odd
number of fields, ignore the last"), so the actual error was silently
discarded and the log line carried no diagnostic detail when the
unsnooze query failed. Pass it as an "error" key/value like everywhere
else in the codebase.
2026-07-30 18:23:26 -07:00
Abhinav Raut f2edc74e18 hardcode copilot name and tidy up AI admin copy 2026-07-31 03:50:39 +05:30
Abhinav Raut 859777dfb4 Merge pull request #447 from abhinavxd/feat/ai-tag-embeddings
shortlist tags with embeddings before asking the LLM to suggest them
2026-07-31 02:46:14 +05:30
Abhinav Raut 52a455f1e8 link message media in one query inside the insert transaction
Message attachments and inline images are now linked with a single UPDATE
that runs in the same transaction as the message insert, so a failure can no
longer leave media orphaned. The inline content_id is stamped by the query
itself, and inlineContentID lowercases the uuid to match it - an uppercase
hex uuid in an image URL used to save a cid the DB never matched, so the
image rendered broken. Drops the now unused Attach and SetContentID.
2026-07-31 02:20:01 +05:30
Abhinav Raut cda53690b0 use shared attachment URL signing in message handlers 2026-07-31 01:55:41 +05:30
Abhinav Raut 75a092514b Fix WS image URLs 2026-07-31 01:26:40 +05:30
Abhinav Raut 205ad8a29e Serve thumbnails inline 2026-07-31 01:20:37 +05:30
Abhinav Raut b553e9625c Keep widget images inline 2026-07-31 01:15:55 +05:30
Abhinav Raut 37224b1030 Clarify thumbnail store handling 2026-07-31 01:11:12 +05:30
Abhinav Raut 79da9e8988 Fix live chat image thumbnails 2026-07-31 01:05:32 +05:30
Abhinav Raut 11d0b523ff bound the tag query by tokens and always clear the tag index on purge 2026-07-30 23:21:07 +05:30
Abhinav Raut 000d789ed8 shortlist tags with embeddings before asking the LLM to suggest them
Tag suggestion used to send the first 300 tags to the model, so anything past
that was invisible. Tags are now embedded into the same index as the knowledge
base, and only the tags most similar to the conversation get sent. The reconcile
loop embeds new and renamed tags and drops vectors for deleted ones, and a
provider change purges them so they are rebuilt with the new model.
2026-07-30 23:09:23 +05:30
Abhinav Raut 16b99069b9 render the generated reply as html instead of showing raw markdown 2026-07-30 16:52:56 +05:30
Abhinav Raut f271462119 dedup mined FAQs against question and answer so near-duplicate suggestions get caught 2026-07-30 00:10:10 +05:30
Abhinav Raut 71fb197826 bump the default completion model off gpt-4o-mini 2026-07-27 02:39:57 +05:30
Abhinav Raut 8db2e34ab5 show specific AI provider errors instead of a generic message 2026-07-27 02:26:33 +05:30
Abhinav Raut e8e2419705 show a clear error when the AI agent runs out of steps and hoist openai response structs 2026-07-27 01:47:06 +05:30
Abhinav Raut b569777cf6 match email quote classes exactly and enforce password length on set password 2026-07-25 21:46:17 +05:30
Abhinav Raut 49c5cb3493 make AI agent max steps and history size configurable
Two AI agent limits were hardcoded in Go: the tool-calling budget per reply
(6) and the number of conversation messages sent to the model as history
(30). Large installs need to tune these, so both are now read from
config.toml as ai_agent.max_steps and ai_agent.max_history_messages.

Defaults are unchanged, so an install that does not set the keys behaves
exactly as before. Values are clamped at the config read layer, 1-20 for
max_steps and 5-100 for history, so a typo cannot burn tokens on a runaway
loop or drop the model's context to nothing.

Also drop the dead re-slice in buildHistory. The message fetch already
limits to max_history_messages and the filter above it only removes
messages, so the second clamp could never fire.
2026-07-25 17:16:59 +05:30
Abhinav Raut 6b8a0f9521 fix content loss in knowledge base chunking and harden AI agent limits
Final review pass before taking the AI agent branch live.

Knowledge base:
- Text not wrapped in a block tag was never collected, so prose around a
  table or list never reached the index. The assistant answered "no
  relevant information" for questions the snippet covered.
- Blocks over the token limit were truncated and the remainder dropped. They
  are split into several chunks now.
- Trimming an oversized block ran one rune at a time and re-tokenized the
  whole string each step. A large table took minutes. It uses a binary
  search now.
- Overlap text was not escaped, so a sentence containing markup swallowed
  the rest of the chunk.
- SVG and template text no longer reaches the index.

AI agent:
- Verification codes are capped per address and per conversation. The cap
  was per conversation only, so a customer correcting a mistyped email was
  told to check an inbox that never got a code.
- Livechat verification sends synchronously. A queued send returned nil even
  when SMTP failed, so a failure counted as a sent code.
- Queued jobs drain on shutdown and hand off to a human instead of being
  dropped with no reply.
- Deleting an assistant no longer moves resolved and closed conversations
  into the fallback team.
- Image decode is capped at 25 MP. The old bound allowed a 400 MB decode per
  attachment.

Auth and admin:
- A blank OIDC client secret no longer overwrites the stored one. Blank id
  or secret is rejected instead.
- OIDC token exchange uses the SSRF guarded client with a timeout.
- Renaming a tool auth header no longer attaches the secret of whichever row
  now sits at that position.
- Clearing embedding dimensions no longer refills 1536 on the next load,
  which pushed a wrong value to the provider on the next save.
- Copilot conversation lookups filter by access before capping at 10.
2026-07-25 03:52:32 +05:30
Abhinav Raut 712fcbb642 return clear 400 errors for invalid AI tool and snippet input
An invalid custom tool name (bad characters or over 64 chars) used to
skip validation, hit the database check constraint, and return a 500.
The name format and length are now checked up front and return a 400
with the name hint, matching how the reserved-name check already works.
Bad tool URLs and parameter JSON now return their own specific message
instead of a generic "Something went wrong". Snippet create and update
now reject an empty title, like they already do for empty content.
Adds a unit test covering the tool validation cases.
2026-07-23 10:00:19 +05:30
Abhinav Raut b8b2b6c9af standardize frontend design system, fix form validation, and skip continuity/CSAT messages in AI context
Design system:
- add semantic success and warning tokens (light + dark) and wire them into Tailwind
- move all hardcoded status colors (green/amber/red) onto tokens across main app and widget; keep file-type icons as identity colors
- give light mode real surface depth: gray chrome sidebars vs white content, deeper canvas gutter, crisper borders, wider gray spread so selected/hover states show
- unify radius (cards rounded-lg, controls rounded-md) and elevation (card shadow-sm, menu shadow-md)
- make reports overview colors uniform: neutral numbers, green met / red breached
- normalize the two page-title heading outliers to text-xl font-semibold

Form fixes:
- require content on AI snippets
- only include non-checkbox prechat fields when they have a value

AI context:
- skip continuity and CSAT messages in AI history, mining, and previous-conversation tools
2026-07-23 03:37:33 +05:30
Abhinav Raut 40f8284e98 strip quoted reply chains from AI agent message context
When the agent built conversation history and mined FAQs, it used the
raw message text, which included the full quoted reply chain from every
email. That wasted tokens and confused the model with old back-and-forth.

Add emailquote.go to strip quoted blocks. HTML messages get their quote
containers pruned (gmail, yahoo, protonmail, outlook markers, and
blockquotes); plain text gets trailing ">" lines and "On ... wrote:" /
"Original Message" markers trimmed. If stripping leaves nothing (a
quote-only reply or forward), we fall back to the full text so the
message is not dropped. Add the matching protonmail_quote selector to
the frontend hide-quoted-text styles so the two stay in sync.

Also fix knowledge base chunking: plain text with no block structure was
kept as one chunk and could overflow the model limit. It now packs into
size-bound pieces on sentence boundaries, and oversized atomic blocks are
flushed and truncated on their own so they can never sneak through.
2026-07-22 21:22:35 +05:30
Abhinav Raut 6125f5ced0 harden AI agent knowledge base, OTP, and prompt injection defenses
Knowledge base: fold the embedding provider base URL into the snippet
fingerprint so re-pointing the provider triggers a reindex even when the
model name is unchanged. Reject empty knowledge base content. Cap
concurrent background snippet embeds at 4 and tie embedding work to the
app lifecycle context. Reindex when the embedding base URL changes, not
just the model or dimensions.

Prompt injection: neutralize << >> block delimiters in snippets,
transcripts, subjects, and contact fields so untrusted content can't
forge a boundary the model relies on.

OTP: set the verified flag inside the Lua match script so verification
is atomic. Only count codes that were actually emailed toward the resend
cap, and check the cap before sending instead of incrementing up front.

Tools: fetch only the enabled tools among the allowed IDs, and route all
registrations through one helper so custom tools can't shadow built-ins.

Agent queue: hand a dropped response job off to a human instead of
leaving the conversation assigned to the assistant with no reply.

Also let GetAllConversationMessages return every message when limit is
non-positive, populate admin forms without triggering validation, and
default the copilot name to Juno.
2026-07-22 14:56:49 +05:30
Abhinav Raut aa200f5850 make the missing AI API key message provider-agnostic 2026-07-22 01:10:32 +05:30
Abhinav Raut ae44e7f4db expand AI tool context and let contacts correct their email
Custom HTTP tools now get more identity context. Each call sends the
contact id, contact type, conversation UUID, and inbox id as headers,
and the contact email is read live per call instead of snapshotting it
at run start.

set_contact_email is no longer blocked once an email is known. A
customer who gives a different email (for example after their account
could not be found) can now correct it. Changing the email clears the
verification flag and any pending code first, so a failed clear can
never leave the conversation verified against an unproven address. The
prompt and tool descriptions were updated to guide this flow.

Also fixes some widget and admin UI issues: prechat form validation
now handles required numbers, checkboxes, and links correctly; tool
header rows keep stable keys so removing a row does not shuffle inputs;
the verification toggle uses form state directly; and the livechat
inbox form shows the inbox UUID with a copy button.
2026-07-22 00:58:41 +05:30
Abhinav Raut 8f8512faf2 apply workspace instructions even with an empty base prompt 2026-07-21 17:35:08 +05:30
Abhinav Raut c74eef079f harden the AI agent run loop and admin lifecycle
Run loop:
- Search now blocks on an indexReady channel until the boot-time embed
  index has loaded, so a run right after startup no longer searches an
  empty index.
- Track the newest message ID each run's history saw (lastSeen). A
  requeued run sees the previous run's own reply as the last message, so
  it now checks for an inbound message that earlier run never included
  instead of bailing out.
- After a run, drop its reply and status changes if a human agent took
  over or resolved the conversation while it was running.
- Register the OTP verification tools on all OTP-based channels even when
  the run starts verified, since the 30-min window can expire mid-run.
- Give PreviewReply a real run timeout.

Admin lifecycle:
- Deleting an assistant now reassigns its conversations to the fallback
  team (or the unassigned queue) so they are not stranded.
- Extract getAssistantRow, validateToolInput and toolParametersOrEmpty
  to share create/update checks.

Misc:
- Surface capped provider error messages to the UI, not just on the admin
  connection test.
- Hard-cap tag suggestions at 3 whatever the model returns.
- Track background reindex goroutines in the wait group so shutdown drains
  them.
- FAQ mining and history fetch now page via GetConversationMessages.
- generateOTP uses stringutil.RandomNumeric.
2026-07-21 17:13:04 +05:30
Abhinav Raut f417aacb92 drain AI workers on shutdown before closing the database
The AI agent and embedding goroutines only stopped on ctx cancellation.
main() never waited for them, so a worker mid-reply could still hit the
database after db.Close() ran.

Give both AI managers a Close() that stops accepting new work and waits for
in-flight workers, matching the existing webhook/sla shutdown pattern. Wire
aiAgent.Close() and ai.Close() into main's shutdown right after the HTTP
server stops, so in-flight work finishes while its dependencies are still up
and before the database closes. Queued-but-unstarted jobs are dropped since
ctx is already cancelled - shutdown stays fast instead of grinding through a
backlog of slow AI calls.
2026-07-21 11:24:53 +05:30
Abhinav Raut ea03339b0d address CodeRabbit review findings in the AI agent
Drop the deprecated webhook.allowed_hosts fallback from the SSRF setup.
It appended hostnames to allowed_cidrs, which only parses CIDRs, so those
entries were silently dropped anyway. Read the [ssrf] block only.

Link AI assistant messages to the specific assistant. postReply now stamps
the assistant id into the message meta, and the message bubble links to that
assistant's edit page, falling back to the assistants list for older messages
that lack the id.
2026-07-21 11:02:43 +05:30
Abhinav Raut 35ddfe5d3e make the image pixel-count guard overflow-safe
Switched the width*height check to a division-based comparison and reject non-positive dimensions, so a crafted header can't wrap the multiplication and slip past the decode cap.
2026-07-21 01:17:56 +05:30
Abhinav Raut 8bedb1a1f4 set OTP send-counter TTL atomically to prevent resend lockout
Incr and Expire were two calls, so if Expire failed the counter key lived forever and the contact could never get another code. A Lua script now does both in one atomic step.
2026-07-21 01:11:57 +05:30
Abhinav Raut c659e30eca address CodeRabbit review findings in the AI agent
- recover from panics in the AI reply and FAQ-mining workers so one bad run can't crash the process
- guard image decode with a pixel-count cap to block image bombs
- hand off (not silently drop) when the confirmation reply fails to send
- give the model a generic tool-failure message instead of the raw error
- log Redis Expire and assistant-cache refresh failures instead of ignoring them
- drop chunk text from RAG debug logs
- refetch the assistant when the edit route's id changes
2026-07-21 01:05:26 +05:30
Abhinav Raut 2fced94268 log when the verification email no-reply guard can't be set 2026-07-21 00:17:23 +05:30
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 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 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 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 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