Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@ package ai.platon.pulsar.agentic.ai.tta

import ai.platon.pulsar.common.ResourceLoader
import ai.platon.pulsar.common.Strings
import ai.platon.pulsar.common.code.ProjectUtils
import ai.platon.pulsar.skeleton.ai.ToolCallSpec
import ai.platon.pulsar.skeleton.common.llm.LLMUtils
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import java.nio.file.Files
import java.nio.file.StandardOpenOption
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.javaMethod

object SourceCodeToToolCallSpec {

Expand Down Expand Up @@ -45,6 +55,155 @@ object SourceCodeToToolCallSpec {
return toolCallSpecs
}

/**
* Generate ToolCallSpec list from a Kotlin interface using reflection.
*
* This method uses Kotlin reflection to inspect the methods of the given interface class,
* extracting method signatures, parameters, return types, and documentation.
* The generated list is then saved as a JSON file in the PROJECT_UTILS.CODE_RESOURCE_DIR directory.
*
* @param domain The domain name for the tool call specs (e.g., "driver")
* @param interfaceClass The Kotlin class representing the interface to inspect
* @param outputFileName The name of the output JSON file (without path)
* @return List of ToolCallSpec objects generated from the interface
*/
fun generateFromReflection(
domain: String,
interfaceClass: KClass<*>,
outputFileName: String = "webdriver-toolcall-specs.json"
): List<ToolCallSpec> {
val toolCallSpecs = mutableListOf<ToolCallSpec>()

// Get all declared functions from the interface
val functions = interfaceClass.declaredFunctions

for (function in functions) {
// Skip private, internal, or deprecated functions if needed
// For now, we'll include all declared functions

val methodName = function.name
val arguments = mutableListOf<ToolCallSpec.Arg>()

// Extract parameters
for (param in function.parameters) {
// Skip the instance parameter (this)
if (param.kind == kotlin.reflect.KParameter.Kind.INSTANCE) {
continue
}

val paramName = param.name ?: "arg${param.index}"
val paramType = extractTypeName(param.type.toString())

// Check if parameter has a default value
val defaultValue = if (param.isOptional) {
// Try to extract default value from parameter
// Note: Reflection doesn't give us the actual default value,
// so we'll use null as a marker for optional parameters
extractDefaultValue(param)
} else {
null
}

arguments.add(ToolCallSpec.Arg(paramName, paramType, defaultValue))
}

// Extract return type
val returnType = extractTypeName(function.returnType.toString())

// Extract KDoc if available
val description = extractKDoc(function)

toolCallSpecs.add(ToolCallSpec(domain, methodName, arguments, returnType, description))
}

// Save to JSON file in CODE_RESOURCE_DIR
saveToJsonFile(toolCallSpecs, outputFileName)

return toolCallSpecs
}

/**
* Extract a simple type name from a full qualified type string.
* For example: "kotlin.String?" -> "String"
*/
private fun extractTypeName(typeString: String): String {
// Remove package names and keep just the class name
val cleaned = typeString
.replace("kotlin.", "")
.replace("java.lang.", "")
.replace("ai.platon.pulsar.", "")

// Handle nullable types
return cleaned.trim()
}

/**
* Extract default value for optional parameters.
* Since Kotlin reflection doesn't provide actual default values,
* we return a placeholder or null.
*/
private fun extractDefaultValue(param: kotlin.reflect.KParameter): String? {
// For optional parameters, we can't get the actual default value via reflection
// We'll return null to indicate it has a default but we don't know what it is
return if (param.isOptional) {
// Return a type-appropriate default marker
when {
param.type.toString().contains("Int") -> "0"
param.type.toString().contains("Boolean") -> "false"
param.type.toString().contains("String") -> "\"\""
else -> null
}
} else {
null
}
}

/**
* Extract KDoc documentation from a function.
* Note: KDoc is not available through Kotlin reflection at runtime,
* so this will return null. For proper documentation, use source code parsing.
*/
private fun extractKDoc(function: KFunction<*>): String? {
// KDoc is not available via reflection at runtime
// To get KDoc, we would need to parse the source file
// For now, return null or try to get from annotations

// Try to get from Deprecated annotation as an example
val deprecated = function.findAnnotation<Deprecated>()
return deprecated?.message
}

/**
* Save ToolCallSpec list to a JSON file in the CODE_RESOURCE_DIR.
*/
private fun saveToJsonFile(toolCallSpecs: List<ToolCallSpec>, fileName: String): Boolean {
val rootDir = ProjectUtils.findProjectRootDir() ?: return false
val destPath = rootDir.resolve(ProjectUtils.CODE_RESOURCE_DIR)

// Ensure directory exists
Files.createDirectories(destPath)

val targetFile = destPath.resolve(fileName)

// Configure ObjectMapper for pretty printing
val objectMapper = ObjectMapper().apply {
enable(SerializationFeature.INDENT_OUTPUT)
}

// Serialize to JSON
val jsonContent = objectMapper.writeValueAsString(toolCallSpecs)

// Write to file
Files.writeString(
targetFile,
jsonContent,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)

return true
}

// Helper types and parsers for SourceCodeToToolCall
private data class ParamSig(val name: String, val type: String, val defaultValue: String?)
private data class FuncSig(val name: String, val params: List<ParamSig>, val returnType: String, val kdoc: String?)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ abstract class AbstractToolExecutor : ToolExecutor {

override suspend fun execute(expression: String, target: Any): TcEvaluate {
val (objectName, functionName, args) = simpleParser.parseFunctionExpression(expression)
?: return TcEvaluate(expression = expression, cause = IllegalArgumentException("Illegal expression"))
?: return TcEvaluate(expression, IllegalArgumentException("Illegal expression"), "Illegal expression")

val tc = ToolCall(objectName, functionName, args)
return execute(tc, target)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class BrowserToolExecutor: AbstractToolExecutor() {
return driver
}

return TcEvaluate(expression, IllegalArgumentException("Unknown expression: $expression, domain: browser"))
return TcEvaluate(expression, IllegalArgumentException("Unknown expression: $expression, domain: browser"), "Unknown expression")
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ai.platon.pulsar.agentic.ai.tta

import ai.platon.pulsar.skeleton.crawl.fetch.driver.WebDriver

/**
* A simple application to generate ToolCallSpec JSON file from WebDriver interface.
*
* This can be run manually to generate the webdriver-toolcall-specs.json file
* in the CODE_RESOURCE_DIR directory.
*/
fun main() {
println("Generating ToolCallSpec from WebDriver interface using reflection...")

val specs = SourceCodeToToolCallSpec.generateFromReflection(
domain = "driver",
interfaceClass = WebDriver::class,
outputFileName = "webdriver-toolcall-specs.json"
)

println("✓ Generated ${specs.size} tool call specs")
println("✓ Saved to: pulsar-core/pulsar-resources/src/main/resources/code-mirror/webdriver-toolcall-specs.json")
println()
println("Sample of generated specs:")
specs.take(5).forEach { spec ->
val args = spec.arguments.joinToString(", ") { arg ->
if (arg.defaultValue != null) {
"${arg.name}: ${arg.type} = ${arg.defaultValue}"
} else {
"${arg.name}: ${arg.type}"
}
}
println(" - ${spec.domain}.${spec.method}($args): ${spec.returnType}")
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package ai.platon.pulsar.agentic.ai.tta

import ai.platon.pulsar.common.ResourceLoader
import ai.platon.pulsar.common.code.ProjectUtils
import ai.platon.pulsar.skeleton.crawl.fetch.driver.WebDriver
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.nio.file.Files
import kotlin.io.path.exists

class SourceCodeToToolCallTest {
@Test
Expand All @@ -14,4 +18,76 @@ class SourceCodeToToolCallTest {
assertNotNull(click, "Should contain driver.click method")
assertTrue(click!!.arguments.map { it.name }.contains("selector"), "click should have selector argument")
}

@Test
fun `generate ToolCallSpec from WebDriver using reflection`() {
// Generate ToolCallSpec list from WebDriver interface using reflection
val toolSpecs = SourceCodeToToolCallSpec.generateFromReflection(
domain = "driver",
interfaceClass = WebDriver::class,
outputFileName = "webdriver-toolcall-specs-test.json"
)

// Verify the list is not empty
assertTrue(toolSpecs.isNotEmpty(), "Generated tool spec list should not be empty")

// Verify the JSON file was created
val rootDir = ProjectUtils.findProjectRootDir()
if (rootDir != null) {
val jsonFile = rootDir.resolve(ProjectUtils.CODE_RESOURCE_DIR)
.resolve("webdriver-toolcall-specs-test.json")
assertTrue(jsonFile.exists(), "JSON file should be created in CODE_RESOURCE_DIR")

// Verify file has content
val fileSize = Files.size(jsonFile)
assertTrue(fileSize > 0, "JSON file should have content")

// Clean up test file
Files.deleteIfExists(jsonFile)
}

// Verify some expected methods exist
val navigateTo = toolSpecs.firstOrNull { it.method == "navigateTo" }
assertNotNull(navigateTo, "Should contain navigateTo method")

val click = toolSpecs.firstOrNull { it.method == "click" }
assertNotNull(click, "Should contain click method")
assertTrue(click!!.arguments.isNotEmpty(), "click should have arguments")

val currentUrl = toolSpecs.firstOrNull { it.method == "currentUrl" }
assertNotNull(currentUrl, "Should contain currentUrl method")

// Verify domain is set correctly
toolSpecs.forEach { spec ->
assertEquals("driver", spec.domain, "All specs should have domain 'driver'")
}
}

@Test
fun `generate and save permanent JSON file for WebDriver`() {
// Generate the actual JSON file that should be committed
val toolSpecs = SourceCodeToToolCallSpec.generateFromReflection(
domain = "driver",
interfaceClass = WebDriver::class,
outputFileName = "webdriver-toolcall-specs.json"
)

// Verify the list is not empty
assertTrue(toolSpecs.isNotEmpty(), "Generated tool spec list should not be empty")
println("Generated ${toolSpecs.size} tool call specs from WebDriver interface")

// Verify the JSON file was created
val rootDir = ProjectUtils.findProjectRootDir()
assertNotNull(rootDir, "Project root directory should be found")

val jsonFile = rootDir!!.resolve(ProjectUtils.CODE_RESOURCE_DIR)
.resolve("webdriver-toolcall-specs.json")
assertTrue(jsonFile.exists(), "JSON file should be created in CODE_RESOURCE_DIR")

// Verify file has content
val fileSize = Files.size(jsonFile)
assertTrue(fileSize > 100, "JSON file should have substantial content (at least 100 bytes)")
println("JSON file created at: ${jsonFile.toAbsolutePath()}")
println("File size: $fileSize bytes")
}
}
Loading