DEV Community

Solon Framework
Solon Framework

Posted on

How We Built an AI After-Sales Engine for E-Commerce Using Solon ReActAgent

All code based on official Solon documentation (article/1285, article/1286, article/1329).

The Problem

Last month, our after-sales support team was drowning in tickets.

Hundreds of "order not received" complaints came in daily. Our CS reps had to switch between the order system, logistics system, and compensation system for every single case. Average handling time: 15 minutes per ticket. During promotions, the backlog hit 3,000+ tickets.

I thought: the workflow is always the same — look up the order, check logistics, determine responsibility, execute compensation. Why not let AI handle this flow?

So I built an AI-driven after-sales decision engine using Solon's ReActAgent.

What Is Solon's AI Agent?

Solon introduced AI Agent capabilities starting from v3.8. The core component is ReActAgent — think of it as an LLM that can call tools.

Instead of just chatting, ReActAgent follows a "Think -> Act -> Observe -> Re-think" loop:

  1. Receives a customer complaint
  2. Think: "I need to look up this order"
  3. Act: Calls get_order(orderId) -> gets order details
  4. Observe: The order has tracking number track_123
  5. Re-think: "Got the tracking number, let me check logistics status"
  6. Act: Calls get_logistic_status(trackNo)
  7. Observe: Status is "lost"
  8. Re-think: "Order amount is $158, > $100, should issue a refund"
  9. Act: Calls apply_compensation("refund", 158)

The entire chain runs without human intervention.

Defining Business Tools

In Solon, tools extend AbsToolProvider with @ToolMapping annotations:

public static class OrderTools extends AbsToolProvider {
    @ToolMapping(description = "Query order details by order ID")
    public String get_order(@Param(description = "Order ID") String orderId) {
        if ("ORD_20251229".equals(orderId)) {
            return "{\"orderId\":\"ORD_20251229\", \"amount\": 158.0, " +
                   "\"trackNo\": \"track_123\", \"sku\": \"Smart Headphones\"}";
        }
        return "{\"error\": \"Order not found\"}";
    }
}

public static class LogisticTools extends AbsToolProvider {
    @ToolMapping(description = "Query shipping status by tracking number")
    public String get_logistic_status(@Param(description = "Tracking number") String trackNo) {
        if ("track_123".equals(trackNo)) {
            return "{\"status\": \"lost\", \"info\": \"Lost at Shanghai center\"}";
        }
        return "{\"error\": \"Tracking number not found\"}";
    }
}

public static class MarketingTools extends AbsToolProvider {
    @ToolMapping(description = "Issue compensation. Small orders: coupon; Large: refund")
    public String apply_compensation(
            @Param(description = "Strategy: coupon or refund") String strategy,
            @Param(description = "Order amount") double amount) {
        if ("refund".equals(strategy) && amount > 100) {
            return "[System] Refund submitted for $" + amount;
        } else if ("coupon".equals(strategy)) {
            return "[System] $20 coupon credited.";
        }
        return "[Manual] Strategy mismatch, escalated to human agent.";
    }
}
Enter fullscreen mode Exit fullscreen mode

Assembling and Running the Agent

ChatModel chatModel = LlmUtil.getChatModel();

ReActAgent agent = ReActAgent.of(chatModel)
        .defaultToolAdd(new OrderTools())
        .defaultToolAdd(new LogisticTools())
        .defaultToolAdd(new MarketingTools())
        .modelOptions(o -> o.temperature(0.0))
        .maxTurns(10)
        .autoRethink(true)
        .build();

AgentSession session = InMemoryAgentSession.of("job_001");
String result = agent.prompt("My order ORD_20251229 hasn't arrived. Can you check?")
    .session(session).call().getContent();
System.out.println(result);
Enter fullscreen mode Exit fullscreen mode

Adding HITL (Human-In-The-Loop) Approval

Refunds involve money. Solon provides HITLInterceptor for human oversight:

HITLInterceptor hitl = new HITLInterceptor()
    .onSensitiveTool("apply_compensation")
    .onTool("apply_compensation", (trace, args) -> {
        double amount = Double.parseDouble(args.get("amount").toString());
        return amount > 100 ? "Large refund requires approval" : null;
    });

ReActAgent agent = ReActAgent.of(chatModel)
        .defaultToolAdd(new OrderTools())
        .defaultToolAdd(new LogisticTools())
        .defaultToolAdd(new MarketingTools())
        .defaultInterceptorAdd(hitl)
        .build();
Enter fullscreen mode Exit fullscreen mode

Approval Endpoints

Endpoint Method Purpose
/ai/hitl/call POST Submit query; returns pending if intercepted
/ai/hitl/task GET Get pending task details
/ai/hitl/submit POST Approve/reject
HITLTask task = HITL.getPendingTask(session);

if ("approve".equals(action)) {
    HITL.submit(session, task.getToolName(),
        HITLDecision.approve().comment("Verified by admin"));
} else {
    HITL.submit(session, task.getToolName(),
        HITLDecision.reject("Risk rejected by admin"));
}

// Resume execution
ReActResponse resp = agent.prompt().session(session).call();
Enter fullscreen mode Exit fullscreen mode

Real-World Impact

Metric Before After
Time per ticket ~15 min <10 sec (auto) / ~2 min (HITL)
CS team workload Manual every ticket Only ~30% need human review
Response to customer Hours Seconds

Final Thoughts

Solon's ReActAgent bridges the gap between people and systems. Clean tool definitions, flexible injection, HITL approval out-of-the-box.

Official docs: article/1285 | article/1286 | article/1329

Top comments (0)