Building a Chatbot with Agentica
typia.llm.application<Class>() gives you the tools an LLM can call. AgenticaΒ builds the agent loop around those tools β the orchestration layer that decides which function to call, when, and how to recover from mistakes.
If typia.llm.application is βdescribe my service to an LLM,β Agentica is βactually run a conversation in which an LLM uses that service.β
First example
import { Agentica } from "@agentica/core";
import OpenAI from "openai";
import typia from "typia";
import { BbsArticleService } from "./BbsArticleService";
const agent = new Agentica({
service: {
api: new OpenAI({ apiKey: "*****" }),
model: "openai/gpt-4.1-mini",
},
controllers: [
typia.llm.controller<BbsArticleService>(
"bbs",
new BbsArticleService(),
),
],
});
await agent.conversate("Hello, I want to create an article.");Thatβs a complete chatbot: it understands intent, picks the right method on BbsArticleService, fills the arguments from the conversation, and asks follow-up questions when arguments are missing. No prompt engineering, no agent graph definition.
@nestia/agent was renamed to @agentica/* to make room for additional packages built on the same idea.
Full source
undefined
import { Agentica } from "@agentica/core";
import OpenAI from "openai";
import typia from "typia";
import { BbsArticleService } from "./BbsArticleService";
const agent = new Agentica({
service: {
api: new OpenAI({ apiKey: "*****" }),
model: "openai/gpt-4.1-mini",
},
controllers: [
typia.llm.controller<BbsArticleService>(
"bbs",
new BbsArticleService(),
),
],
});
await agent.conversate("Hello, I want to create an article.");- Live demo: nestia.io/chat/bbsΒ
From an OpenAPI document
You donβt have to start from TypeScript code. Agentica accepts an OpenAPI document directly β every endpoint becomes a tool the LLM can call.
import { Agentica, assertHttpController } from "@agentica/core";
import OpenAI from "openai";
import typia from "typia";
import { MobileFileSystem } from "./services/MobileFileSystem";
const agent = new Agentica({
vendor: {
api: new OpenAI({ apiKey: "********" }),
model: "openai/gpt-4.1-mini",
},
controllers: [
// Class-based functions
typia.llm.controller<MobileFileSystem>("filesystem", new MobileFileSystem()),
// Swagger / OpenAPI document β functions
assertHttpController({
name: "shopping",
document: await fetch(
"https://shopping-be.wrtn.ai/editor/swagger.json",
).then((r) => r.json()),
connection: {
host: "https://shopping-be.wrtn.ai",
headers: { Authorization: "Bearer ********" },
},
}),
],
});
await agent.conversate("I wanna buy MacBook Pro");- Live demo: nestia.io/chat/shopping
- Backend source: github.com/samchon/shopping-backendΒ
- Swagger UI: open in @nestia/editorΒ
How the loop works
Three sub-agents share the work:
- Selector β reads the latest user message, decides which of the registered functions are candidates this turn. If none apply, it falls back to plain conversational replies.
- Caller β given the candidate functions, tries to call them. If the conversation hasnβt given it enough information for the parameters, it asks the user a follow-up.
- Describer β once the caller has produced results, turns them into a human-readable reply.
That separation is what keeps Agentica simple: each sub-agent does one thing and you donβt have to draw a state graph to keep it on track.
Validation feedback
LLM function calling is not perfect. Models make type mistakes β for example, they fill an Array<string> field with just a string. The fix is to give the model the structured error and ask it to retry.
Agentica relies on the same harness as typia.llm.application:
import { FunctionCall } from "pseudo";
import { ILlmFunction, IValidation } from "typia";
export const correctFunctionCall = (p: {
call: FunctionCall;
functions: Array<ILlmFunction<"chatgpt">>;
retry: (reason: string, errors?: IValidation.IError[]) => Promise<unknown>;
}): Promise<unknown> => {
const func = p.functions.find((f) => f.name === p.call.name);
if (func === undefined) {
return p.retry("Unable to find the matched function name. Try it again.");
}
const result: IValidation<unknown> = func.validate(p.call.arguments);
if (!result.success) {
// 1st trial: ~50% pass (gpt-4o-mini on shopping mall demo)
// 2nd trial with validation feedback: ~99%
// 3rd trial with validation feedback again: never failed in my testing
return p.retry(
"Type errors are detected. Correct it through validation errors.",
result.errors,
);
}
return result.data;
};The numbers in the comments come from running the shopping chatbot demo above. The pattern works for the same reason it works in application and at AutoBeΒ β typiaβs validator gives the LLM enough detail (path, expected type, actual value) to fix its own mistakes.
Some LLM providers donβt even honor all the JSON Schema constraint keywords. The validation feedback channel works around that too: the model doesnβt have to understand "format": "uuid" β it just sees expected: "string & Format<\"uuid\">" in the error and tries again. The keywords most commonly ignored by providers, grouped by base type:
stringβminLength,maxLength,pattern,format,contentMediaTypenumberβminimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOfarrayβminItems,maxItems,uniqueItems,items
typia checks every one of these at runtime and surfaces failures into the feedback channel β so whether or not the provider taught the model to honor the schema keyword in the first place, the model gets a chance to fix the value on the next turn.
| Components | typia | TypeBox | ajv | io-ts | zod | C.V. |
|---|---|---|---|---|---|---|
| Easy to use | β | β | β | β | β | β |
| Object (simple)Β | β | β | β | β | β | β |
| Object (hierarchical)Β | β | β | β | β | β | β |
| Object (recursive)Β | β | β | β | β | β | β |
| Object (union, implicit)Β | β | β | β | β | β | β |
| Object (union, explicit)Β | β | β | β | β | β | β |
| Object (additional tags)Β | β | β | β | β | β | β |
| Object (template literal types)Β | β | β | β | β | β | β |
| Object (dynamic properties)Β | β | β | β | β | β | β |
| Array (rest tuple)Β | β | β | β | β | β | β |
| Array (hierarchical)Β | β | β | β | β | β | β |
| Array (recursive)Β | β | β | β | β | β | β |
| Array (recursive, union)Β | β | β | β | β | β | β |
| Array (R+U, implicit)Β | β | β | β | β | β | β |
| Array (repeated)Β | β | β | β | β | β | β |
| Array (repeated, union)Β | β | β | β | β | β | β |
| Ultimate Union Type | β | β | β | β | β | β |
C.V.meansclass-validator
OpenAPI pipeline
Agentica accepts Swagger 2.0 / OpenAPI 3.0 / 3.1. The conversion goes through an intermediate βemendedβ 3.1 representation that strips out the ambiguity and duplication of the public OpenAPI specs, then to a migration schema, and finally to whichever LLM providerβs function-calling format you target.
Why the intermediate step? Without it, youβd need nΓm converters (every OpenAPI version Γ every LLM provider). With it, you need n+m.
Where to go next
- The class-based tool API β
typia.llm.application - The OpenAPI-based tool API β
HttpLlm - Validation feedback mechanics β
LlmJson - Documentation tips for better LLM behavior β Documentation Strategy
- Agentica itself β github.com/wrtnlabs/agenticaΒ