Appearance
Architecture Overview
Opsome is a multi-tenant Laravel application. Each tenant is a store. Each store has three independently-configured adapters.
Request flow (inbound message)
Customer message (Chatwoot)
↓
POST /webhook/chatwoot/{tenantId}
↓ verify HMAC, filter events, normalize
InboundMessageDTO
↓
ProcessInboundMessageJob
↓ resolve tenant → load adapters
↓ fetch context (orders, products, conversation history)
↓ run LLM agent (confidence ≥ 80%)
↓
ApprovalRequest (status: pending)
+ internal note posted to Chatwoot
↓ store owner approves in dashboard
ExecuteApprovalJob
↓
ChatwootChannelAdapter::sendReply()Tenant adapter configuration
Each tenant stores three adapter configs (encrypted) in the tenants table:
tenants.commerce_adapter → { type: 'lunarphp', base_url: '...', token: '...' }
tenants.product_adapter → { type: 'google_sheets', sheet_id: '...', service_account: {...} }
tenants.channel_adapter → { type: 'chatwoot', base_url: '...', api_key: '...', account_id: '...' }An AdapterFactory resolves the correct implementation from the type field.
Key design constraints
- Adapters never create approval requests — only read or execute after approval
- Identity is always resolved from the channel contact profile — never from message content
- All writes are approval-gated except
assignConversation(internal, reversible) - Confidence threshold: ≥80% — below this the agent escalates to human queue
Agent configuration
Each tenant stores an agent_config (encrypted) alongside the adapter configs:
tenants.agent_config → {
provider: "openrouter",
model: "zhipu/glm-4-flash",
api_key: "<encrypted>",
history_window: 10,
store_name: "buy2give.store",
store_tone: "friendly, professional"
}The SupportAgentService builds a lean system prompt from store_name + store_tone + output schema rules, then injects a runtime <context> block containing orders, last history_window messages, and any product hits.
The LLM returns a single JSON response:
json
{
"intent": "order_status | refund_request | product_qa | other",
"action_type": "reply | escalate | refund | cancel | resolve",
"confidence": 85,
"draft": "Hi Sarah, your order shipped yesterday...",
"internal_note": "Customer asking about delay. Order shows shipped."
}Hard overrides applied in code: refund and cancel action types always force confidence = 0, ensuring they enter the human approval queue unconditionally.
Directory structure
app/
Adapters/
Commerce/ LunarPHPCommerceAdapter, WooCommerceAdapter, ...
Product/ GoogleSheetsProductAdapter, AkeneoProductAdapter, ...
Channel/ ChatwootChannelAdapter, FreshchatChannelAdapter, ...
Contracts/ CommerceAdapterContract, ProductAdapterContract, ChannelAdapterContract
LlmClientContract
DTOs/ OrderDTO, CustomerDTO, ConversationDTO, MessageDTO, ProductDTO,
InboundMessageDTO, AgentResponseDTO
Http/
Controllers/
Webhook/ ChatwootWebhookController, ...
Jobs/ ProcessInboundMessageJob, ExecuteApprovalJob
Models/ Tenant, ApprovalRequest, User
Services/ TenantContext, AdapterFactory, SupportAgentService
Llm/ OpenRouterLlmClient