Recently, we released a "teacher search agent" feature at Manalink, an online tutoring platform. This AI agent helps users find the perfect teacher through natural language conversations.
Here's a quick demo of the feature:
Note: Screen shown is from development phase / Search results are examples
Key Features
The AI teacher search agent includes these distinctive capabilities:
- Character-by-character streaming responses similar to ChatGPT, creating a familiar UX
- Natural language input processing - users can describe their needs in plain language, and the AI understands subjects, grade levels, and other criteria
- Unstructured data search - beyond typical filters like subjects and grades, users can search using criteria like "needs help with communication skills" or "prefers strict teaching style"
- Rich structured UI components - instead of plain text responses, the system displays structured cards with teacher rankings, recommendations, and detailed information through JSON streaming
- Real-time progress indicators - users see live updates as the AI searches through various data sources, reducing perceived wait time
- Interactive conversations - users can continue refining their search based on initial results
Technical Architecture Overview
To achieve these features, we implemented several key technical decisions:
- AI Agent approach instead of simple LLM API calls for flexible, context-aware responses
- Mastra framework as our AI agent foundation, leveraging its streaming capabilities and existing tooling
- Next.js and React Native integration with proper AWS infrastructure configuration for streaming
- Embedding and RAG for similarity search to handle unstructured data without consuming excessive context
- Careful tool, schema, and instruction design to ensure stable agent behavior
-
Custom
useStructuredChat
hook extending Vercel AI SDK'suseChat
for cross-platform structured data streaming - Business-focused accuracy metrics ensuring the agent meets real-world educational matching requirements
This article focuses primarily on implementing structured data streaming for AI agents while touching on UX considerations and the broader challenges of agent development.
Environment Information
This implementation uses:
- June 2025 timeframe
- Next.js 14.x
- @mastra/core and related Mastra packages 0.10.x
- Expo 52.x
The Challenge of Structured Data Streaming
Why Streaming Matters
Before diving into technical implementation, let's establish why streaming is essential and why it needs to be structured.
User Experience Benefits:
AI generation, despite optimizations, takes time. For tasks users perceive as "doable myself" or "available elsewhere," even 10 seconds feels excessive. Streaming display eliminates the "waiting" sensation by showing progressive results.
Changed User Expectations:
Major AI services (ChatGPT, Claude, Gemini) have made streaming standard. Like how LINE popularized chat UI as a communication standard, users now expect AI responses to stream. This creates a baseline expectation developers must meet.
Why Structure Matters
Rich UI and Interactions:
Structured data (JSON) enables direct mapping to React components. For example:
- Plain text: Restaurant name, description, URL as text
- Structured data: Restaurant name, rating, image URL, reservation link, availability button - enabling rich restaurant cards with actionable elements
Application Integration:
Structured output enables sophisticated workflows. JSON responses can trigger additional data fetching via SWR, enable conditional UI rendering, and support complex application logic that plain text cannot facilitate.
Implementation Approaches
Option 1: Basic useChat
Mastra internally uses Vercel AI SDK, making the useChat
hook directly available. However, this approach outputs plain text only. Even with JSON format prompts, streaming creates incomplete JSON fragments like { "name": "Jo
that cannot be parsed until completion, defeating streaming benefits.
During development, I experimented with custom parsing rules where {{teacherId:123456789}}
in AI output would render as <TeacherCard teacherId={123456789} />
components. While functional, this approach can cause visual artifacts and imposes external constraints.
Option 2: useObject
Hook
The useObject
hook represents the primary structured streaming solution for non-agent scenarios:
import { mastra } from "@/src/mastra";
export async function POST(req: Request) {
const body = await req.json();
const myAgent = mastra.getAgent("weatherAgent");
const stream = await myAgent.stream(body, {
output: z.object({
weather: z.string(),
}),
});
return stream.toTextStreamResponse();
}
import { experimental_useObject as useObject } from '@ai-sdk/react';
export default function Page() {
const { object, submit } = useObject({
api: '/api/use-object',
schema: z.object({
weather: z.string(),
}),
});
return (
<div>
<button onClick={() => submit('example input')}>Generate</button>
{object?.weather && <p>{object.weather}</p>}
</div>
);
}
However, with agent calls, intermediate tool invocations don't match the schema, preventing users from seeing progress updates—only final results appear.
Option 3: Custom Hook Development
After investigating various approaches and reviewing GitHub discussions, I developed a custom solution using Vercel AI SDK's parsePartialJson
utility function—the same function powering useObject
internally.
Here's the core implementation:
parts: message.parts.map((part) => {
if (part.type === 'text') {
const parsedMessage = parsePartialJson(part.text);
if (['repaired-parse', 'successful-parse'].includes(parsedMessage.state)) {
return {
type: 'output',
structuredData: parsedMessage.value as PartialSchema,
};
}
The solution involves parsing partial JSON at each streaming step. The parsePartialJson
function intelligently repairs incomplete JSON:
-
{ "name": "Jo
becomes{ "name": "Jo" }
-
{ "array": [1, 2,
becomes{ "array": [1, 2] }
-
{ "nested": { "foo": "bar
becomes{ "nested": { "foo": "bar" } }
This approach requires handling DeepPartial<z.infer<TSchema>>
types on the frontend, with appropriate undefined checks.
For tool invocation progress display, use experimental_output
instead of output
in agent calls:
const result = await mastra.getAgent('manalinkAgent').stream(messages, {
experimental_output: ResponseSchema,
});
return result.toDataStreamResponse();
Frontend Implementation
UI rendering requires careful handling of partial data:
{messages.map((message) => (
<div key={message.id}>
<div>{message.role === 'user' ? 'User' : 'AI'}</div>
{message.parts.map((part, i) => {
switch (part.type) {
case 'output':
const value = part.structuredData;
if (!value?.teachers) return null;
return (
<div>
{value.teachers
.filter((teacher) => teacher?.id)
.map((teacher) => (
<div key={teacher.id}>
<h3>
{teacher.ranking && <span>{teacher.ranking}位</span>}
<span>{teacher.name}</span>
</h3>
{teacher.ranking && (
<p>Reason: {teacher.rankReason}</p>
)}
<p>{teacher.recommendation}</p>
<TeacherCard
id={teacher.id}
subjectIds={teacher.subjectIds}
gradeId={teacher.gradeId}
/>
</div>
))}
</div>
);
case 'tool-invocation':
return (
<p key={`${message.id}-${i}`}>
{getToolDisplayName(part.toolInvocation.toolName)}
{loading && <span>...</span>}
</p>
);
default:
return null;
}
})}
</div>
))}
Technical Challenges and Solutions
Application-Level Barriers
Next.js App Router Requirement:
Integrating Mastra requires App Router API Routes. While our main app uses Pages Router, we can call App Router APIs from Pages Router components.
Expo 52 Fetch Requirement:
React Native streaming requires Expo 52's enhanced fetch API for proper streaming support.
Infrastructure Considerations
Local Development Setup:
Our Nginx-fronted local environment required specific configuration:
location /api/ {
proxy_pass {Next.js host and port};
proxy_buffering off; # Critical for streaming
}
Production Deployment:
Network configuration becomes complex in staging/production environments. Early deployment testing is crucial for streaming and AI agent implementations.
AI Agent Tool Design Philosophy
Structured vs. Unstructured Data Search
We distinguish between two search types:
- Structured data: Subject preferences, grade levels—existing database fields that can be filtered programmatically
- Unstructured data: "Communication difficulties," "strict teaching style"—requiring embedding and vector search
Balancing Mechanical and AI-Driven Processing
Rather than providing many primitive tools to the agent, we implemented fewer tools that combine mechanical filtering with vector search. This approach prevents hallucinations in areas requiring precise filtering while leveraging AI strengths for similarity matching and recommendation generation.
Our Tool Strategy:
- Mechanical filtering: Handle programmatically to ensure accuracy
- AI processing: Query generation, similarity scoring, recommendation writing, result ranking
Agile Tool Development
Tool design requires iterative development:
- Build basic tools quickly
- Integrate with agent early
- Observe agent behavior
- Refine based on results
Avoid over-engineering individual tools—focus on overall agent effectiveness.
Performance and Monitoring
Speed Optimization
Tool input schema length significantly impacts generation time. Keep tool inputs concise and use memory management to avoid verbose tool calls.
For RAG implementation, we balanced search accuracy with speed, prioritizing reasonable response times over perfect precision given the inherent complexity of teacher-student matching.
Logging and Observability
Mastra provides Pino-based JSON logging for CloudWatch integration:
export const mastraLogger = new PinoLogger({
name: 'Mastra',
level: process.env.NODE_ENV === 'production' ? 'info' :
(process.env.AI_AGENT_LOG_LEVEL as LogLevel | undefined) ?? 'info',
overrideDefaultTransports: true,
});
Project Management and Timeline
Resource Allocation
Total effort: 2 person-months
- Web Engineer: Vector data processing, embedding research, RAG prototyping
- Myself: Everything else (streaming implementation, agent design, UI integration)
Risk Management Strategy
Key approach: Maintain non-agent alternatives until final commitment
Since our team lacked agent development experience, I ensured fallback options remained viable throughout development. This allowed critical evaluation of agent necessity while building stakeholder confidence through progressive demonstrations.
Team Education and Buy-in
Technical Education:
- Internal workshops on agent concepts
- Hands-on MCP server development
- Cursor/coding agent analysis sessions
Stakeholder Engagement:
- Visual demonstrations of agent capabilities
- Canary releases for internal testing
- Progressive feature reveals showing agent advantages
Results and Learnings
Benefits of Structured Streaming
- Rich UI variety: Different data types enable tailored visual designs
- Contextual messaging: Conditional messages (e.g., no-results explanations) with custom styling
- Clean schema separation: Agent prompts focus on business logic while schemas handle data structure
Key Takeaways
- Agent development feels like extending familiar coding assistants to new domains
- Structured streaming significantly enhances user experience beyond plain text responses
- Iterative tool development proves more effective than upfront perfect design
- Cross-platform considerations (Next.js + React Native) require careful infrastructure planning
- Team education and progressive demonstration essential for stakeholder buy-in
Looking Forward
AI agents represent a natural evolution from the coding assistants we use daily. The combination of streaming responses, structured data, and thoughtful tool design creates genuinely useful applications that enhance rather than replace human decision-making.
For developers interested in agent development, start with your daily coding assistant—understand its tool usage patterns, experiment with MCP servers, and gradually build familiarity with agent behavior patterns.
Interested in AI agent development discussions?
Connect with me on X or Pitta - I'd love to hear about your agent projects and challenges!
Top comments (1)
Looks quite interesting
Some comments may only be visible to logged-in visitors. Sign in to view all comments.