DEV Community

Solon Framework
Solon Framework

Posted on

Building an E-Commerce After-Sales AI Engine with 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: product name, amount, tracking number")
    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 real-time 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\": \"Package lost at Shanghai distribution center\"}";
        }
        return "{\"error\": \"Tracking number not found\"}";
    }
}

public static class MarketingTools extends AbsToolProvider {
    @ToolMapping(description = "Issue compensation based on policy. " +
            "Small orders (<=100): coupon; Large orders (>100): full 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
                    + ". Expected to process within 24 hours.";
        } else if ("coupon".equals(strategy)) {
            return "[System] $20 coupon credited to user account.";
        }
        return "[Manual] Strategy mismatch, escalated to human agent.";
    }
}
Enter fullscreen mode Exit fullscreen mode

The key is @ToolMapping—it tells the LLM what each tool does and what parameters it needs.

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("demo_customer_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

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 user query; returns pending approval task if intercepted
/ai/hitl/task GET Get current pending task details
/ai/hitl/submit POST Approve/reject/modify parameters
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 AI 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. Docs: article/1285, article/1286, article/1329.

Top comments (0)