DEV Community

Solon Framework
Solon Framework

Posted on

Solon AI Talents: Building Modular, Context-Aware AI Agents Beyond Simple Tools

Solon AI Talents: Building Modular, Context-Aware AI Agents Beyond Simple Tools

If you've built AI agents before, you've probably hit this wall: your tool list grows too long, the model gets confused about when to use what, and your code turns into a tangled mess of if-else branching just to decide which tools to expose.

I've been there. And after spending some time with Solon's AI framework, I found something that changed how I think about agent architecture: Talents.

The Problem with Plain Tool Orchestration

Let's say you're building an internal support agent. It needs to:

  • Search the knowledge base
  • Check order status
  • Reset user passwords
  • Escalate to admin (for admins only)
  • Access billing info (for finance team only)

With plain tools, you'd typically do:

model.prompt("I need to reset my password")
    .options(o -> {
        o.toolAdd(new KbSearchTool());
        o.toolAdd(new OrderStatusTool());
        o.toolAdd(new PasswordResetTool());
        if ("admin".equals(role)) {
            o.toolAdd(new AdminEscalationTool());
        }
        if ("finance".equals(role)) {
            o.toolAdd(new BillingTool());
        }
    })
    .call();
Enter fullscreen mode Exit fullscreen mode

This works, but it's fragile. The business logic is scattered across call sites. The model sees 5+ tools even when the user just wants to reset a password. And when you have 20+ tools, the model's reasoning quality drops noticeably.

What Are Talents?

Solon Talents are self-contained capability modules — think of them as mini-agents within your agent. Each Talent bundles three things:

  1. Awareness (isSupported) — Can I handle this request?
  2. Instruction (getInstruction) — What SOP should the model follow when using me?
  3. Execution (getTools) — What tools do I expose?

When you register Talents, the framework automatically:

  • Filters inactive Talents based on user intent (saves tokens)
  • Injects only relevant instructions into the System Message
  • "Colors" each tool with its Talent's metadata — so the model knows exactly which SOP applies to which tool

Defining a Talent

There are two ways to build a Talent. I'll show both.

Way 1: Declarative with TalentDesc (Quick & Dynamic)

When you need something fast and don't want to create a new class:

TalentDesc orderExpert = new TalentDesc("order_expert")
    .description("Order management assistant")
    // Only activate when user mentions orders
    .isSupported(prompt -> 
        prompt.getUserContent().contains("order"))
    // Dynamic instruction based on user level
    .instruction(prompt -> {
        if ("VIP".equals(prompt.attr("user_level"))) {
            return "This is a VIP customer. Prioritize fast-track processing.";
        }
        return "Process orders using standard procedures.";
    })
    .toolAdd(new OrderQueryTool())
    .toolAdd(new OrderCancelTool());
Enter fullscreen mode Exit fullscreen mode

Then register it:

ChatModel model = ChatModel.of(config)
    .defaultTalentAdd(orderExpert)
    .build();
Enter fullscreen mode Exit fullscreen mode

Way 2: OOP with AbsTalent (For Complex Logic)

When you need dependency injection, state management, or complex business logic:

@Component
public class TechSupportTalent extends AbsTalent {

    @Inject
    private KbService kbService;

    @Override
    public String name() {
        return "tech_support";
    }

    @Override
    public String description() {
        return "Technical support specialist — handles troubleshooting and config resets";
    }

    @Override
    public boolean isSupported(Prompt prompt) {
        String content = prompt.getUserContent();
        return content != null && (
            content.contains("error") ||
            content.contains("bug") ||
            content.contains("故障")
        );
    }

    @Override
    public String getInstruction(Prompt prompt) {
        return """
            You are a technical support specialist. Follow these SOPs:
            1. First search the knowledge base for known solutions
            2. Verify the user's Solon version before suggesting fixes
            3. If modifying production config, clearly state the risks
            """;
    }

    @ToolMapping(name = "search_kb", 
                 description = "Search the knowledge base for solutions")
    public String searchKb(@Param("query") String query) {
        return kbService.search(query);
    }

    @ToolMapping(
        name = "reset_config",
        description = "Reset system configuration (high-risk operation)",
        meta = "{'danger': true, 'confirm_msg': 'Service will restart. Proceed?'}"
    )
    public String resetConfig(@Param("serviceName") String serviceName) {
        return "Service [" + serviceName + "] config reset, restarting...";
    }
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Context Injection with Prompt Attrs

This is where Talents really shine. The Prompt object carries runtime attributes — invisible to the LLM, but accessible to your Talent logic.

// Build a prompt with business context
Prompt prompt = Prompt.of("I need to check order #12345")
    .attrPut("tenant_id", "tenant_abc")
    .attrPut("user_role", "ADMIN")
    .attrPut("user_level", "VIP");

model.prompt(prompt).call();
Enter fullscreen mode Exit fullscreen mode

Inside your Talent, you can access these:

public class OrderManagerTalent implements Talent {

    @Override
    public boolean isSupported(Prompt prompt) {
        // Only activate if we have a valid tenant context
        return prompt.attr("tenant_id") != null 
            && prompt.getUserContent().contains("order");
    }

    @Override
    public String getInstruction(Prompt prompt) {
        String tenant = prompt.attrOrDefault("tenant_name", "unknown");
        return "You are the order manager for [" + tenant + 
               "]. Only handle this tenant's data.";
    }

    @Override
    public Collection<FunctionTool> getTools(Prompt prompt) {
        List<FunctionTool> tools = new ArrayList<>();
        tools.add(new OrderQueryTool());

        // Only admins get the cancel tool
        if ("ADMIN".equals(prompt.attr("user_role"))) {
            tools.add(new OrderCancelTool());
        }

        return tools;
    }
}
Enter fullscreen mode Exit fullscreen mode

Registering Talents — Global vs Per-Request

Global registration (for always-on capabilities)

ChatModel model = ChatModel.of(config)
    .defaultTalentAdd(securityAuditTalent)   // always active
    .defaultTalentAdd(0, translationTalent)  // with priority (index)
    .build();
Enter fullscreen mode Exit fullscreen mode

Per-request injection (for dynamic scenarios)

model.prompt("What's the weather in Tokyo?")
    .options(o -> {
        o.talentAdd(weatherTalent);
        o.talentAdd(2, languageTalent); // with priority
    })
    .call();
Enter fullscreen mode Exit fullscreen mode

Priority matters

Talents are processed in registration order. Instructions are accumulated into the System Message sequentially. The framework "colors" each tool with its Talent's identity, so the model associates the right SOP with the right tool set.

When to Use Talents vs Plain Tools

Scenario Use Tools Use Talents
Simple stateless function
Needs SOP instructions
Dynamic activation by context
Permission-based tool filtering
Single-purpose utility
Multi-tool coordination

Rule of thumb: If your tool needs more than a description to explain when and how to use it, wrap it in a Talent.

Putting It All Together: A Multi-Tenant Support Agent

Here's a complete example showing how Talents make multi-tenant support clean:

@Component
public class TenantAwareSupportAgent {

    @Inject
    private TenantService tenantService;

    private final ChatModel model;

    public TenantAwareSupportAgent() {
        this.model = ChatModel.of(config)
            .defaultTalentAdd(new OrderManagerTalent())
            .defaultTalentAdd(new TechSupportTalent())
            .defaultTalentAdd(new BillingTalent())
            .build();
    }

    public String handle(String userMessage, String tenantId, String role) {
        // Load tenant context
        Tenant tenant = tenantService.findById(tenantId);

        Prompt prompt = Prompt.of(userMessage)
            .attrPut("tenant_id", tenant.id())
            .attrPut("tenant_name", tenant.name())
            .attrPut("user_role", role);

        ChatResponse resp = model.prompt(prompt).call();
        return resp.getMessage();
    }
}
Enter fullscreen mode Exit fullscreen mode

The calling code is clean. The Talent logic is self-contained. And the model only sees the tools relevant to the current request.

The One Thing I Wish I Knew Earlier

Talents aren't just "tools with instructions." They're decision boundaries for your AI. Each Talent draws a clear line: "I handle this domain, and here's exactly how." The model doesn't have to guess which tool to use — the Talent's SOP guides it precisely.

This is especially noticeable when you have 10+ tools. Before Talents, the model would occasionally call the wrong tool or try to use a tool outside its intended context. After moving to Talents with isSupported guards, those issues largely disappeared.

Next Steps

The Solon AI docs have more depth on:

If you're building production AI agents with Solon, Talents are the architecture pattern you didn't know you needed. Try wrapping your next tool set into a Talent — the difference in model behavior is immediate.

Top comments (0)