Step 5 of 5
Build a Research Assistant agent with search tool, summarization, source citation
Workshop — สร้าง Research Assistant agent ที่ search, summarize, cite sources
Build a Research Assistant that takes a question, searches for information (mocked), summarizes the findings, and cites its sources. This brings together tools, system prompts, and the agent loop.
search tool that returns a few results with titles, snippets, and URLsDiagram: The Research Assistant flow.
Loading diagram...
Model one search result as a data class. Return it as a readable string so the LLM can use it.
data class SearchResult(
val title: String,
val snippet: String,
val url: String
) {
override fun toString() = "[$title] $snippet ($url)"
}
Implement ToolSet. The tool returns a few hardcoded results. Later you can replace the body with a real HTTP call.
class ResearchTools : ToolSet {
private val mockIndex = mapOf(
"kotlin coroutines" to listOf(
SearchResult(
"Coroutines overview",
"Coroutines are lightweight threads for async work.",
"https://kotlinlang.org/coroutines"
),
SearchResult(
"Structured concurrency",
"Each async operation runs inside a scope.",
"https://kotlinlang.org/structured-concurrency"
)
)
)
@Tool
@LLMDescription("Search the web for relevant pages. Use this before answering.")
fun search(
@LLMDescription("The search query") query: String
): String {
val key = mockIndex.keys.firstOrNull { query.contains(it, ignoreCase = true) }
val results = mockIndex[key].orEmpty()
if (results.isEmpty()) return "No results for '$query'."
return results.mapIndexed { i, r -> "${i + 1}. $r" }.joinToString("\n")
}
}
The system prompt is the key. It tells the LLM to search first, then summarize with numbered citations.
import kotlinx.coroutines.runBlocking
fun researchAgent() = AIAgent(
promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY") ?: error("Set OPENAI_API_KEY")),
llmModel = OpenAIModels.Chat.GPT4o,
systemPrompt = """
You are a research assistant.
1. Always call the 'search' tool before answering.
2. Summarize what you found in 3-4 sentences.
3. Add a 'Sources:' section with numbered citations matching the results.
""".trimIndent(),
toolRegistry = ToolRegistry { tools(ResearchTools()) },
maxIterations = 8
)
fun main() = runBlocking {
val agent = researchAgent()
println(agent.run("How do Kotlin coroutines work?"))
}
Expected output:
Kotlin coroutines are lightweight units of async work that run on a few
real threads. Each operation belongs to a scope, which keeps concurrent
work structured and easy to cancel.
Sources:
1. Coroutines overview (https://kotlinlang.org/coroutines)
2. Structured concurrency (https://kotlinlang.org/structured-concurrency)
Test the tool logic directly, without the LLM. This is fast and deterministic.
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ResearchToolsTest {
private val tools = ResearchTools()
@Test
fun `known query returns results`() {
val out = tools.search("kotlin coroutines")
assertTrue(out.contains("Coroutines overview"))
assertTrue(out.contains("1."))
}
@Test
fun `unknown query returns no results`() {
assertEquals("No results for 'mars rovers'.", tools.search("mars rovers"))
}
}
Run with ./gradlew test. To test the full agent without spending tokens, mock the promptExecutor (Koog lets you inject a fake executor that returns canned responses).
OPENAI_API_KEY.main.search, then produce a cited summary.fetchPage(url) tool that returns more text from a URL, or a calculator tool for numeric questions.search body with a real HTTP call to a search API.You now have a complete, testable tool-using agent in Koog.