feat(extensions): aurelio-jetbrains model/config system, document collaboration, flint-chart integration
This commit is contained in:
parent
84eac19e9d
commit
447540866b
11 changed files with 4960 additions and 0 deletions
|
|
@ -0,0 +1,303 @@
|
|||
// Configuration structure for Aurelio Agent
|
||||
// Defines all configurable parameters for the agent across different platforms
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
/**
|
||||
* Main configuration class for Aurelio Agent
|
||||
*/
|
||||
data class AgentConfiguration(
|
||||
val general: GeneralConfig = GeneralConfig(),
|
||||
val aiProviders: AIProvidersConfig = AIProvidersConfig(),
|
||||
val mcpServers: MCPServersConfig = MCPServersConfig(),
|
||||
val chat: ChatConfig = ChatConfig(),
|
||||
val appearance: AppearanceConfig = AppearanceConfig(),
|
||||
val privacy: PrivacyConfig = PrivacyConfig()
|
||||
)
|
||||
|
||||
/**
|
||||
* General configuration settings
|
||||
*/
|
||||
data class GeneralConfig(
|
||||
val agentName: String = "Aurelio",
|
||||
val agentIdentity: String = "Portugal Futurista AI Assistant",
|
||||
val enableTelemetry: Boolean = true,
|
||||
val autoUpdate: Boolean = true,
|
||||
val language: String = "en",
|
||||
val timezone: String = "UTC",
|
||||
val theme: String = "dark"
|
||||
)
|
||||
|
||||
/**
|
||||
* AI Providers configuration
|
||||
*/
|
||||
data class AIProvidersConfig(
|
||||
val hermesNvidia: NvidiaNimConfig = NvidiaNimConfig(),
|
||||
val localOllama: OllamaConfig = OllamaConfig(),
|
||||
val openAi: OpenAIConfig = OpenAIConfig(),
|
||||
val anthropic: AnthropicConfig = AnthropicConfig(),
|
||||
val googleVertex: GoogleVertexConfig = GoogleVertexConfig(),
|
||||
val mistral: MistralConfig = MistralConfig(),
|
||||
val together: TogetherConfig = TogetherConfig(),
|
||||
val localLmStudio: LmStudioConfig = LmStudioConfig()
|
||||
)
|
||||
|
||||
/**
|
||||
* NVIDIA NIM (Hermes Agent) configuration
|
||||
*/
|
||||
data class NvidiaNimConfig(
|
||||
val enabled: Boolean = false,
|
||||
val apiKey: String = "",
|
||||
val baseUrl: String = "https://integrate.api.nvidia.com/v1",
|
||||
val defaultModel: String = "nvidia/nemotron-3-ultra-550b-a55b",
|
||||
val temperature: Double = 0.7,
|
||||
val maxTokens: Int = 4096
|
||||
)
|
||||
|
||||
/**
|
||||
* Ollama configuration
|
||||
*/
|
||||
data class OllamaConfig(
|
||||
val enabled: Boolean = true,
|
||||
val baseUrl: String = "http://localhost:11434",
|
||||
val defaultModel: String = "llama3.2",
|
||||
val temperature: Double = 0.7,
|
||||
val maxTokens: Int = 2048
|
||||
)
|
||||
|
||||
/**
|
||||
* OpenAI configuration
|
||||
*/
|
||||
data class OpenAIConfig(
|
||||
val enabled: Boolean = false,
|
||||
val apiKey: String = "",
|
||||
val baseUrl: String = "https://api.openai.com/v1",
|
||||
val defaultModel: String = "gpt-4o",
|
||||
val temperature: Double = 0.7,
|
||||
val maxTokens: Int = 4096
|
||||
)
|
||||
|
||||
/**
|
||||
* Anthropic configuration
|
||||
*/
|
||||
data class AnthropicConfig(
|
||||
val enabled: Boolean = false,
|
||||
val apiKey: String = "",
|
||||
val baseUrl: String = "https://api.anthropic.com/v1",
|
||||
val defaultModel: String = "claude-3-5-sonnet-20241022",
|
||||
val temperature: Double = 0.7,
|
||||
val maxTokens: Int = 4096
|
||||
)
|
||||
|
||||
/**
|
||||
* Google Vertex configuration
|
||||
*/
|
||||
data class GoogleVertexConfig(
|
||||
val enabled: Boolean = false,
|
||||
val apiKey: String = "",
|
||||
val baseUrl: String = "https://generativelanguage.googleapis.com/v1beta",
|
||||
val defaultModel: String = "gemini-2.0-flash-exp",
|
||||
val temperature: Double = 0.7,
|
||||
val maxTokens: Int = 2048
|
||||
)
|
||||
|
||||
/**
|
||||
* Mistral configuration
|
||||
*/
|
||||
data class MistralConfig(
|
||||
val enabled: Boolean = false,
|
||||
val apiKey: String = "",
|
||||
val baseUrl: String = "https://api.mistral.ai/v1",
|
||||
val defaultModel: String = "mistral-large-latest",
|
||||
val temperature: Double = 0.7,
|
||||
val maxTokens: Int = 2048
|
||||
)
|
||||
|
||||
/**
|
||||
* Together AI configuration
|
||||
*/
|
||||
data class TogetherConfig(
|
||||
val enabled: Boolean = false,
|
||||
val apiKey: String = "",
|
||||
val baseUrl: String = "https://api.together.xyz/v1",
|
||||
val defaultModel: String = "meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo",
|
||||
val temperature: Double = 0.7,
|
||||
val maxTokens: Int = 2048
|
||||
)
|
||||
|
||||
/**
|
||||
* LM Studio configuration
|
||||
*/
|
||||
data class LmStudioConfig(
|
||||
val enabled: Boolean = false,
|
||||
val baseUrl: String = "http://localhost:1234/v1",
|
||||
val defaultModel: String = "local-model",
|
||||
val temperature: Double = 0.7,
|
||||
val maxTokens: Int = 2048
|
||||
)
|
||||
|
||||
/**
|
||||
* MCP Servers configuration
|
||||
*/
|
||||
data class MCPServersConfig(
|
||||
val flintChart: FlintChartMCPConfig = FlintChartMCPConfig(),
|
||||
val electricalMcp: ElectricalMCPConfig = ElectricalMCPConfig(),
|
||||
val julesMcp: JulesMCPConfig = JulesMCPConfig(),
|
||||
val olhosDeOrpheu: OlhosDeOrpheuConfig = OlhosDeOrpheuConfig()
|
||||
)
|
||||
|
||||
/**
|
||||
* Flint Chart MCP configuration
|
||||
*/
|
||||
data class FlintChartMCPConfig(
|
||||
val enabled: Boolean = true,
|
||||
val baseUrl: String = "http://localhost:8085",
|
||||
val port: Int = 8085,
|
||||
val chartStoragePath: String = "./charts",
|
||||
val maxCharts: Int = 100
|
||||
)
|
||||
|
||||
/**
|
||||
* Electrical MCP configuration
|
||||
*/
|
||||
data class ElectricalMCPConfig(
|
||||
val enabled: Boolean = true,
|
||||
val baseUrl: String = "http://localhost:8081",
|
||||
val port: Int = 8081
|
||||
)
|
||||
|
||||
/**
|
||||
* Jules MCP configuration
|
||||
*/
|
||||
data class JulesMCPConfig(
|
||||
val enabled: Boolean = true,
|
||||
val baseUrl: String = "http://localhost:8082",
|
||||
val port: Int = 8082
|
||||
)
|
||||
|
||||
/**
|
||||
* Olhos De Orpheu MCP configuration
|
||||
*/
|
||||
data class OlhosDeOrpheuConfig(
|
||||
val enabled: Boolean = false,
|
||||
val baseUrl: String = "http://192.168.0.16:8001/sse",
|
||||
val useAsGateway: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Chat configuration
|
||||
*/
|
||||
data class ChatConfig(
|
||||
val enableMarkdown: Boolean = true,
|
||||
val enableCodeHighlighting: Boolean = true,
|
||||
val enableChartEmbedding: Boolean = true,
|
||||
val maxHistory: Int = 100,
|
||||
val autoSaveChats: Boolean = true,
|
||||
val chatStoragePath: String = "./chats",
|
||||
val enableVoiceInput: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Appearance configuration
|
||||
*/
|
||||
data class AppearanceConfig(
|
||||
val theme: String = "dark",
|
||||
val fontSize: Int = 14,
|
||||
val fontFamily: String = "default",
|
||||
val sidebarPosition: String = "left",
|
||||
val compactMode: Boolean = false,
|
||||
val showAvatars: Boolean = true,
|
||||
val agentAvatar: String = "default"
|
||||
)
|
||||
|
||||
/**
|
||||
* Privacy configuration
|
||||
*/
|
||||
data class PrivacyConfig(
|
||||
val enableLocalProcessing: Boolean = true,
|
||||
val disableCloudSync: Boolean = false,
|
||||
val dataRetentionDays: Int = 30,
|
||||
val anonymizeTelemetry: Boolean = true,
|
||||
val disableUsageLogging: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Interface for configuration persistence
|
||||
*/
|
||||
interface ConfigurationPersistence {
|
||||
suspend fun loadConfiguration(): AgentConfiguration
|
||||
suspend fun saveConfiguration(config: AgentConfiguration)
|
||||
suspend fun validateConfiguration(config: AgentConfiguration): List<String>
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation for configuration persistence
|
||||
*/
|
||||
class DefaultConfigurationPersistence : ConfigurationPersistence {
|
||||
private val configFilePath = System.getProperty("user.home") + "/.aurelio/config.json"
|
||||
private val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
|
||||
|
||||
override suspend fun loadConfiguration(): AgentConfiguration {
|
||||
val file = java.io.File(configFilePath)
|
||||
return if (file.exists()) {
|
||||
try {
|
||||
objectMapper.readValue(file, AgentConfiguration::class.java)
|
||||
} catch (e: Exception) {
|
||||
println("Error loading configuration: ${e.message}")
|
||||
AgentConfiguration() // Return default config
|
||||
}
|
||||
} else {
|
||||
AgentConfiguration() // Return default config
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveConfiguration(config: AgentConfiguration) {
|
||||
val file = java.io.File(configFilePath)
|
||||
file.parentFile?.mkdirs()
|
||||
objectMapper.writeValue(file, config)
|
||||
}
|
||||
|
||||
override suspend fun validateConfiguration(config: AgentConfiguration): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
// Validate API keys for enabled providers
|
||||
if (config.aiProviders.openAi.enabled && config.aiProviders.openAi.apiKey.isBlank()) {
|
||||
errors.add("OpenAI API key is required when OpenAI provider is enabled")
|
||||
}
|
||||
|
||||
if (config.aiProviders.anthropic.enabled && config.aiProviders.anthropic.apiKey.isBlank()) {
|
||||
errors.add("Anthropic API key is required when Anthropic provider is enabled")
|
||||
}
|
||||
|
||||
if (config.aiProviders.googleVertex.enabled && config.aiProviders.googleVertex.apiKey.isBlank()) {
|
||||
errors.add("Google Vertex API key is required when Google Vertex provider is enabled")
|
||||
}
|
||||
|
||||
if (config.aiProviders.mistral.enabled && config.aiProviders.mistral.apiKey.isBlank()) {
|
||||
errors.add("Mistral API key is required when Mistral provider is enabled")
|
||||
}
|
||||
|
||||
if (config.aiProviders.together.enabled && config.aiProviders.together.apiKey.isBlank()) {
|
||||
errors.add("Together API key is required when Together provider is enabled")
|
||||
}
|
||||
|
||||
if (config.aiProviders.hermesNvidia.enabled && config.aiProviders.hermesNvidia.apiKey.isBlank()) {
|
||||
errors.add("NVIDIA NIM API key is required when NVIDIA NIM provider is enabled")
|
||||
}
|
||||
|
||||
// Validate ports
|
||||
if (config.mcpServers.flintChart.port < 1024 || config.mcpServers.flintChart.port > 65535) {
|
||||
errors.add("Flint Chart MCP port must be between 1024 and 65535")
|
||||
}
|
||||
|
||||
if (config.mcpServers.electricalMcp.port < 1024 || config.mcpServers.electricalMcp.port > 65535) {
|
||||
errors.add("Electrical MCP port must be between 1024 and 65535")
|
||||
}
|
||||
|
||||
if (config.mcpServers.julesMcp.port < 1024 || config.mcpServers.julesMcp.port > 65535) {
|
||||
errors.add("Jules MCP port must be between 1024 and 65535")
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
// Main Aurelio Model System Entry Point
|
||||
// Integrates ModelRouter, ConfigResolver, and MixtureFallbackManager
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Main class that coordinates the Aurelio model system
|
||||
*/
|
||||
class AurelioModelSystem {
|
||||
private val configResolver = ConfigResolver()
|
||||
private val modelRouter = ModelRouter()
|
||||
private val mixtureFallbackManager = MixtureFallbackManager(modelRouter)
|
||||
|
||||
init {
|
||||
// Load configuration on initialization
|
||||
configResolver.loadConfiguration()
|
||||
println("[AurelioModelSystem] Initialized with config: ${configResolver.getConfig()}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a chat request using the configured routing and fallback strategies
|
||||
*/
|
||||
suspend fun processChatRequest(
|
||||
messages: List<ChatMessage>,
|
||||
providerOverride: String? = null,
|
||||
mixtureStrategy: MixtureStrategy = MixtureStrategy.WEIGHTED_AVERAGE,
|
||||
fallbackStrategy: FallbackStrategy = FallbackStrategy.SEQUENTIAL
|
||||
): ModelResult {
|
||||
val config = configResolver.getConfig()
|
||||
|
||||
// Determine which providers to use
|
||||
val providersToUse = if (providerOverride != null) {
|
||||
listOf(providerOverride)
|
||||
} else {
|
||||
// Use configured default provider and its fallbacks
|
||||
listOf(config.defaultProvider) + modelRouter.getAvailableProviders().filter {
|
||||
it != config.defaultProvider
|
||||
}
|
||||
}
|
||||
|
||||
println("[AurelioModelSystem] Processing chat request with providers: $providersToUse")
|
||||
|
||||
return if (config.enableMixtures && providersToUse.size > 1) {
|
||||
// Use mixture strategy when enabled and multiple providers available
|
||||
val mixtureResult = mixtureFallbackManager.executeMixtureRequest(
|
||||
messages,
|
||||
providersToUse,
|
||||
mixtureStrategy
|
||||
)
|
||||
|
||||
// Convert mixture result to model result
|
||||
ModelResult(
|
||||
success = mixtureResult.success,
|
||||
content = mixtureResult.content,
|
||||
modelUsed = "mixture-${mixtureResult.mixtureStrategy.name.lowercase()}",
|
||||
provider = mixtureResult.combinedResponses.firstOrNull()?.provider ?: ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = mixtureResult.totalLatencyMs,
|
||||
error = if (!mixtureResult.success) "Mixture operation failed" else null
|
||||
)
|
||||
} else {
|
||||
// Use fallback strategy
|
||||
mixtureFallbackManager.executeFallbackRequest(
|
||||
messages,
|
||||
providersToUse,
|
||||
fallbackStrategy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a chat request with multiple providers simultaneously using mixture strategies
|
||||
*/
|
||||
suspend fun processMixtureRequest(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>? = null,
|
||||
mixtureStrategy: MixtureStrategy = MixtureStrategy.WEIGHTED_AVERAGE
|
||||
): MixtureResult {
|
||||
val config = configResolver.getConfig()
|
||||
|
||||
val selectedProviders = providers ?: if (config.enableMixtures) {
|
||||
// Use all configured providers for mixture
|
||||
modelRouter.getAvailableProviders()
|
||||
} else {
|
||||
// Just use the default provider
|
||||
listOf(config.defaultProvider)
|
||||
}
|
||||
|
||||
return mixtureFallbackManager.executeMixtureRequest(
|
||||
messages,
|
||||
selectedProviders,
|
||||
mixtureStrategy
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a request with fallback strategies
|
||||
*/
|
||||
suspend fun processFallbackRequest(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>? = null,
|
||||
fallbackStrategy: FallbackStrategy = FallbackStrategy.SEQUENTIAL
|
||||
): ModelResult {
|
||||
val selectedProviders = providers ?: modelRouter.getAvailableProviders()
|
||||
|
||||
return mixtureFallbackManager.executeFallbackRequest(
|
||||
messages,
|
||||
selectedProviders,
|
||||
fallbackStrategy
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system statistics
|
||||
*/
|
||||
fun getSystemStats(): Map<String, Any> {
|
||||
val routerStats = modelRouter.getStats()
|
||||
val healthStatus = mixtureFallbackManager.getAllHealthStatus()
|
||||
|
||||
return mapOf(
|
||||
"routerStats" to routerStats,
|
||||
"healthStatus" to healthStatus,
|
||||
"availableProviders" to modelRouter.getAvailableProviders(),
|
||||
"configuration" to configResolver.getConfig()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh system state (e.g., health checks)
|
||||
*/
|
||||
suspend fun refreshSystem() {
|
||||
mixtureFallbackManager.refreshHealthStatus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom provider configuration
|
||||
*/
|
||||
fun addProviderConfig(name: String, config: ProviderConfig) {
|
||||
modelRouter.addProviderConfig(name, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying model router
|
||||
*/
|
||||
fun getModelRouter(): ModelRouter {
|
||||
return modelRouter
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying mixture/fallback manager
|
||||
*/
|
||||
fun getMixtureFallbackManager(): MixtureFallbackManager {
|
||||
return mixtureFallbackManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration resolver
|
||||
*/
|
||||
fun getConfigResolver(): ConfigResolver {
|
||||
return configResolver
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to create and use the Aurelio Model System
|
||||
*/
|
||||
suspend fun useAurelioModelSystem(block: suspend AurelioModelSystem.() -> Unit) {
|
||||
val system = AurelioModelSystem()
|
||||
block(system)
|
||||
}
|
||||
|
||||
/**
|
||||
* Example usage of the Aurelio Model System
|
||||
*/
|
||||
suspend fun main() {
|
||||
println("Initializing Aurelio Model System...")
|
||||
|
||||
useAurelioModelSystem { system ->
|
||||
// Example chat messages
|
||||
val messages = listOf(
|
||||
ChatMessage("system", "You are a helpful AI assistant for the Portugal Futurista project."),
|
||||
ChatMessage("user", "What are the goals of the Portugal Futurista initiative?")
|
||||
)
|
||||
|
||||
println("\nProcessing request with default configuration...")
|
||||
|
||||
// Process a regular request
|
||||
val result = system.processChatRequest(messages)
|
||||
|
||||
println("\nResult:")
|
||||
println("Success: ${result.success}")
|
||||
println("Model used: ${result.modelUsed}")
|
||||
println("Provider: ${result.provider}")
|
||||
println("Latency: ${result.latencyMs}ms")
|
||||
if (result.error != null) {
|
||||
println("Error: ${result.error}")
|
||||
} else {
|
||||
println("Content preview: ${result.content.take(200)}${if (result.content.length > 200) "..." else ""}")
|
||||
}
|
||||
|
||||
// Get system stats
|
||||
println("\nSystem Stats:")
|
||||
val stats = system.getSystemStats()
|
||||
println("Total requests: ${stats["routerStats"] as? Map<*, *> ?: "N/A"}")
|
||||
println("Available providers: ${stats["availableProviders"]}")
|
||||
|
||||
// Process with mixture strategy
|
||||
if (system.getModelRouter().getAvailableProviders().size > 1) {
|
||||
println("\nTrying mixture request...")
|
||||
val mixtureResult = system.processMixtureRequest(
|
||||
messages,
|
||||
mixtureStrategy = MixtureStrategy.BEST_OF_N
|
||||
)
|
||||
|
||||
println("Mixture success: ${mixtureResult.success}")
|
||||
println("Combined responses: ${mixtureResult.combinedResponses.size}")
|
||||
}
|
||||
}
|
||||
|
||||
println("\nAurelio Model System example completed.")
|
||||
}
|
||||
233
extensions/aurelio-jetbrains/src/main/kotlin/ConfigResolver.kt
Normal file
233
extensions/aurelio-jetbrains/src/main/kotlin/ConfigResolver.kt
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
// Configuration Resolver for Aurelio Model Router
|
||||
// Handles loading and resolving configuration for different AI providers
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
|
||||
/**
|
||||
* Configuration for the model router system
|
||||
*/
|
||||
data class ModelRouterConfig(
|
||||
val defaultProvider: String = "hermes-nvidia",
|
||||
val enableFallbacks: Boolean = true,
|
||||
val enableMixtures: Boolean = true,
|
||||
val mixtureWeights: Map<String, Double> = mapOf(
|
||||
"hermes-nvidia" to 0.3,
|
||||
"local-ollama" to 0.2,
|
||||
"openai" to 0.15,
|
||||
"anthropic" to 0.15,
|
||||
"google-vertex" to 0.1,
|
||||
"mistral" to 0.1
|
||||
),
|
||||
val providerTimeoutMs: Long = 30000,
|
||||
val maxRetries: Int = 3,
|
||||
val enableLoadBalancing: Boolean = true,
|
||||
val healthCheckIntervalMs: Long = 60000
|
||||
)
|
||||
|
||||
/**
|
||||
* Resolves configuration from various sources with precedence:
|
||||
* 1. Environment variables
|
||||
* 2. Configuration file
|
||||
* 3. Default values
|
||||
*/
|
||||
class ConfigResolver {
|
||||
private val objectMapper: ObjectMapper = jacksonObjectMapper()
|
||||
private var config: ModelRouterConfig = ModelRouterConfig()
|
||||
|
||||
companion object {
|
||||
// Configuration file locations to check
|
||||
private val CONFIG_LOCATIONS = listOf(
|
||||
"./aurelio-config.json",
|
||||
"~/.aurelio/config.json",
|
||||
"/etc/aurelio/config.json",
|
||||
"./config/aurelio-config.json"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from all available sources
|
||||
*/
|
||||
fun loadConfiguration(): ModelRouterConfig {
|
||||
// Start with defaults
|
||||
var loadedConfig = ModelRouterConfig()
|
||||
|
||||
// Try to load from file
|
||||
val configFile = findConfigFile()
|
||||
if (configFile != null) {
|
||||
loadedConfig = loadFromFile(configFile)
|
||||
}
|
||||
|
||||
// Override with environment variables
|
||||
loadedConfig = applyEnvironmentOverrides(loadedConfig)
|
||||
|
||||
// Store the final config
|
||||
this.config = loadedConfig
|
||||
|
||||
return loadedConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the configuration file in standard locations
|
||||
*/
|
||||
private fun findConfigFile(): File? {
|
||||
for (location in CONFIG_LOCATIONS) {
|
||||
val expandedLocation = expandUserHome(location)
|
||||
val file = File(expandedLocation)
|
||||
if (file.exists() && file.canRead()) {
|
||||
println("[ConfigResolver] Found config file: ${file.absolutePath}")
|
||||
return file
|
||||
}
|
||||
}
|
||||
println("[ConfigResolver] No config file found in standard locations")
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from a JSON file
|
||||
*/
|
||||
private fun loadFromFile(file: File): ModelRouterConfig {
|
||||
try {
|
||||
val content = file.readText()
|
||||
val fileConfig = objectMapper.readValue<ModelRouterConfig>(content)
|
||||
println("[ConfigResolver] Loaded config from file: ${file.absolutePath}")
|
||||
return fileConfig
|
||||
} catch (e: Exception) {
|
||||
println("[ConfigResolver] Failed to load config from ${file.absolutePath}: ${e.message}")
|
||||
return ModelRouterConfig()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply environment variable overrides to the configuration
|
||||
*/
|
||||
private fun applyEnvironmentOverrides(config: ModelRouterConfig): ModelRouterConfig {
|
||||
var updatedConfig = config
|
||||
|
||||
// Override default provider
|
||||
System.getenv("AURELIO_DEFAULT_PROVIDER")?.let { defaultProvider ->
|
||||
updatedConfig = updatedConfig.copy(defaultProvider = defaultProvider)
|
||||
println("[ConfigResolver] Overriding default provider to: $defaultProvider")
|
||||
}
|
||||
|
||||
// Override fallback enablement
|
||||
System.getenv("AURELIO_ENABLE_FALLBACKS")?.let { enableFallbacks ->
|
||||
updatedConfig = updatedConfig.copy(enableFallbacks = enableFallbacks.lowercase() == "true")
|
||||
println("[ConfigResolver] Overriding enableFallbacks to: ${updatedConfig.enableFallbacks}")
|
||||
}
|
||||
|
||||
// Override mixtures enablement
|
||||
System.getenv("AURELIO_ENABLE_MIXTURES")?.let { enableMixtures ->
|
||||
updatedConfig = updatedConfig.copy(enableMixtures = enableMixtures.lowercase() == "true")
|
||||
println("[ConfigResolver] Overriding enableMixtures to: ${updatedConfig.enableMixtures}")
|
||||
}
|
||||
|
||||
// Override provider timeout
|
||||
System.getenv("AURELIO_PROVIDER_TIMEOUT_MS")?.let { timeoutStr ->
|
||||
try {
|
||||
val timeout = timeoutStr.toLong()
|
||||
updatedConfig = updatedConfig.copy(providerTimeoutMs = timeout)
|
||||
println("[ConfigResolver] Overriding provider timeout to: $timeout ms")
|
||||
} catch (e: NumberFormatException) {
|
||||
println("[ConfigResolver] Invalid timeout value: $timeoutStr")
|
||||
}
|
||||
}
|
||||
|
||||
// Override max retries
|
||||
System.getenv("AURELIO_MAX_RETRIES")?.let { retriesStr ->
|
||||
try {
|
||||
val retries = retriesStr.toInt()
|
||||
updatedConfig = updatedConfig.copy(maxRetries = retries)
|
||||
println("[ConfigResolver] Overriding max retries to: $retries")
|
||||
} catch (e: NumberFormatException) {
|
||||
println("[ConfigResolver] Invalid retries value: $retriesStr")
|
||||
}
|
||||
}
|
||||
|
||||
// Override load balancing
|
||||
System.getenv("AURELIO_ENABLE_LOAD_BALANCING")?.let { enableLoadBalancing ->
|
||||
updatedConfig = updatedConfig.copy(enableLoadBalancing = enableLoadBalancing.lowercase() == "true")
|
||||
println("[ConfigResolver] Overriding enableLoadBalancing to: ${updatedConfig.enableLoadBalancing}")
|
||||
}
|
||||
|
||||
// Override health check interval
|
||||
System.getenv("AURELIO_HEALTH_CHECK_INTERVAL_MS")?.let { intervalStr ->
|
||||
try {
|
||||
val interval = intervalStr.toLong()
|
||||
updatedConfig = updatedConfig.copy(healthCheckIntervalMs = interval)
|
||||
println("[ConfigResolver] Overriding health check interval to: $interval ms")
|
||||
} catch (e: NumberFormatException) {
|
||||
println("[ConfigResolver] Invalid health check interval value: $intervalStr")
|
||||
}
|
||||
}
|
||||
|
||||
return updatedConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand user home directory in path (~)
|
||||
*/
|
||||
private fun expandUserHome(path: String): String {
|
||||
return if (path.startsWith("~/")) {
|
||||
Paths.get(System.getProperty("user.home"), path.substring(2)).toString()
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
fun getConfig(): ModelRouterConfig {
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload configuration
|
||||
*/
|
||||
fun reload(): ModelRouterConfig {
|
||||
return loadConfiguration()
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the configuration
|
||||
*/
|
||||
fun validateConfig(config: ModelRouterConfig): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
if (config.mixtureWeights.values.sum() > 1.0 && config.enableMixtures) {
|
||||
errors.add("Mixture weights sum exceeds 1.0")
|
||||
}
|
||||
|
||||
if (config.providerTimeoutMs <= 0) {
|
||||
errors.add("Provider timeout must be positive")
|
||||
}
|
||||
|
||||
if (config.maxRetries < 0) {
|
||||
errors.add("Max retries cannot be negative")
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a sample configuration file
|
||||
*/
|
||||
fun generateSampleConfig(): String {
|
||||
val sampleConfig = ModelRouterConfig()
|
||||
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(sampleConfig)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to get configuration with caching
|
||||
*/
|
||||
fun getConfig(): ModelRouterConfig {
|
||||
val resolver = ConfigResolver()
|
||||
return resolver.loadConfiguration()
|
||||
}
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
// Configuration Manager for Aurelio Agent
|
||||
// Handles loading, saving, validation, and updates to agent configuration
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* Listener interface for configuration changes
|
||||
*/
|
||||
interface ConfigurationListener {
|
||||
fun onConfigurationChanged(oldConfig: AgentConfiguration, newConfig: AgentConfiguration)
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager class for handling agent configuration
|
||||
*/
|
||||
class ConfigurationManager {
|
||||
private val mutex = Mutex()
|
||||
private var currentConfig: AgentConfiguration = AgentConfiguration()
|
||||
private val listeners = CopyOnWriteArrayList<ConfigurationListener>()
|
||||
private val persistence: ConfigurationPersistence = DefaultConfigurationPersistence()
|
||||
|
||||
init {
|
||||
// Load initial configuration
|
||||
runBlocking {
|
||||
currentConfig = persistence.loadConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
suspend fun getCurrentConfig(): AgentConfiguration = mutex.withLock {
|
||||
currentConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the configuration
|
||||
*/
|
||||
suspend fun updateConfiguration(newConfig: AgentConfiguration): ConfigurationUpdateResult {
|
||||
val validationResult = persistence.validateConfiguration(newConfig)
|
||||
if (validationResult.isNotEmpty()) {
|
||||
return ConfigurationUpdateResult(
|
||||
success = false,
|
||||
errors = validationResult,
|
||||
warnings = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
val oldConfig = mutex.withLock {
|
||||
val old = currentConfig
|
||||
currentConfig = newConfig
|
||||
old
|
||||
}
|
||||
|
||||
// Save the new configuration
|
||||
try {
|
||||
persistence.saveConfiguration(newConfig)
|
||||
} catch (e: Exception) {
|
||||
// Revert to old config if save fails
|
||||
mutex.withLock {
|
||||
currentConfig = oldConfig
|
||||
}
|
||||
return ConfigurationUpdateResult(
|
||||
success = false,
|
||||
errors = listOf("Failed to save configuration: ${e.message}"),
|
||||
warnings = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
// Notify listeners of configuration change
|
||||
notifyListeners(oldConfig, newConfig)
|
||||
|
||||
return ConfigurationUpdateResult(
|
||||
success = true,
|
||||
errors = emptyList(),
|
||||
warnings = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a configuration listener
|
||||
*/
|
||||
fun addConfigurationListener(listener: ConfigurationListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a configuration listener
|
||||
*/
|
||||
fun removeConfigurationListener(listener: ConfigurationListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all listeners of configuration change
|
||||
*/
|
||||
private fun notifyListeners(oldConfig: AgentConfiguration, newConfig: AgentConfiguration) {
|
||||
listeners.forEach { listener ->
|
||||
try {
|
||||
listener.onConfigurationChanged(oldConfig, newConfig)
|
||||
} catch (e: Exception) {
|
||||
println("Error notifying configuration listener: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset configuration to defaults
|
||||
*/
|
||||
suspend fun resetToDefaults(): ConfigurationUpdateResult {
|
||||
val defaultConfig = AgentConfiguration()
|
||||
return updateConfiguration(defaultConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Import configuration from a file
|
||||
*/
|
||||
suspend fun importConfiguration(filePath: String): ConfigurationUpdateResult {
|
||||
try {
|
||||
val file = java.io.File(filePath)
|
||||
if (!file.exists()) {
|
||||
return ConfigurationUpdateResult(
|
||||
success = false,
|
||||
errors = listOf("Configuration file does not exist: $filePath"),
|
||||
warnings = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
|
||||
val importedConfig = objectMapper.readValue(file, AgentConfiguration::class.java)
|
||||
|
||||
return updateConfiguration(importedConfig)
|
||||
} catch (e: Exception) {
|
||||
return ConfigurationUpdateResult(
|
||||
success = false,
|
||||
errors = listOf("Failed to import configuration: ${e.message}"),
|
||||
warnings = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export current configuration to a file
|
||||
*/
|
||||
suspend fun exportConfiguration(filePath: String): ConfigurationUpdateResult {
|
||||
try {
|
||||
val file = java.io.File(filePath)
|
||||
file.parentFile?.mkdirs()
|
||||
|
||||
val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
|
||||
objectMapper.writeValue(file, currentConfig)
|
||||
|
||||
return ConfigurationUpdateResult(
|
||||
success = true,
|
||||
errors = emptyList(),
|
||||
warnings = emptyList()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return ConfigurationUpdateResult(
|
||||
success = false,
|
||||
errors = listOf("Failed to export configuration: ${e.message}"),
|
||||
warnings = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration validation errors
|
||||
*/
|
||||
suspend fun getValidationErrors(): List<String> {
|
||||
return persistence.validateConfiguration(currentConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific provider is enabled
|
||||
*/
|
||||
suspend fun isProviderEnabled(provider: ModelProvider): Boolean {
|
||||
val config = getCurrentConfig()
|
||||
return when (provider) {
|
||||
ModelProvider.HERMES_NVIDIA_NIM -> config.aiProviders.hermesNvidia.enabled
|
||||
ModelProvider.LOCAL_OLLAMA -> config.aiProviders.localOllama.enabled
|
||||
ModelProvider.OPENAI -> config.aiProviders.openAi.enabled
|
||||
ModelProvider.ANTHROPIC -> config.aiProviders.anthropic.enabled
|
||||
ModelProvider.GOOGLE_VERTEX -> config.aiProviders.googleVertex.enabled
|
||||
ModelProvider.MISTRAL -> config.aiProviders.mistral.enabled
|
||||
ModelProvider.TOGETHER -> config.aiProviders.together.enabled
|
||||
ModelProvider.LOCAL_LM_STUDIO -> config.aiProviders.localLmStudio.enabled
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider configuration
|
||||
*/
|
||||
suspend fun getProviderConfig(provider: ModelProvider): Any? {
|
||||
val config = getCurrentConfig()
|
||||
return when (provider) {
|
||||
ModelProvider.HERMES_NVIDIA_NIM -> config.aiProviders.hermesNvidia
|
||||
ModelProvider.LOCAL_OLLAMA -> config.aiProviders.localOllama
|
||||
ModelProvider.OPENAI -> config.aiProviders.openAi
|
||||
ModelProvider.ANTHROPIC -> config.aiProviders.anthropic
|
||||
ModelProvider.GOOGLE_VERTEX -> config.aiProviders.googleVertex
|
||||
ModelProvider.MISTRAL -> config.aiProviders.mistral
|
||||
ModelProvider.TOGETHER -> config.aiProviders.together
|
||||
ModelProvider.LOCAL_LM_STUDIO -> config.aiProviders.localLmStudio
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MCP server configuration
|
||||
*/
|
||||
suspend fun getMcpServerConfig(serverName: String): Any? {
|
||||
val config = getCurrentConfig()
|
||||
return when (serverName) {
|
||||
"flint-chart" -> config.mcpServers.flintChart
|
||||
"electrical-mcp" -> config.mcpServers.electricalMcp
|
||||
"jules-mcp" -> config.mcpServers.julesMcp
|
||||
"olhos-de-orpheu" -> config.mcpServers.olhosDeOrpheu
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a subset of configuration for UI purposes
|
||||
*/
|
||||
suspend fun getGeneralConfig(): GeneralConfig = mutex.withLock {
|
||||
currentConfig.general
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AI providers configuration
|
||||
*/
|
||||
suspend fun getAIProvidersConfig(): AIProvidersConfig = mutex.withLock {
|
||||
currentConfig.aiProviders
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MCP servers configuration
|
||||
*/
|
||||
suspend fun getMCPServersConfig(): MCPServersConfig = mutex.withLock {
|
||||
currentConfig.mcpServers
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chat configuration
|
||||
*/
|
||||
suspend fun getChatConfig(): ChatConfig = mutex.withLock {
|
||||
currentConfig.chat
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appearance configuration
|
||||
*/
|
||||
suspend fun getAppearanceConfig(): AppearanceConfig = mutex.withLock {
|
||||
currentConfig.appearance
|
||||
}
|
||||
|
||||
/**
|
||||
* Get privacy configuration
|
||||
*/
|
||||
suspend fun getPrivacyConfig(): PrivacyConfig = mutex.withLock {
|
||||
currentConfig.privacy
|
||||
}
|
||||
|
||||
/**
|
||||
* Update specific configuration sections
|
||||
*/
|
||||
suspend fun updateGeneralConfig(newGeneral: GeneralConfig): ConfigurationUpdateResult {
|
||||
val current = getCurrentConfig()
|
||||
val updatedConfig = current.copy(general = newGeneral)
|
||||
return updateConfiguration(updatedConfig)
|
||||
}
|
||||
|
||||
suspend fun updateAIProvidersConfig(newAIProviders: AIProvidersConfig): ConfigurationUpdateResult {
|
||||
val current = getCurrentConfig()
|
||||
val updatedConfig = current.copy(aiProviders = newAIProviders)
|
||||
return updateConfiguration(updatedConfig)
|
||||
}
|
||||
|
||||
suspend fun updateMCPServersConfig(newMCPServers: MCPServersConfig): ConfigurationUpdateResult {
|
||||
val current = getCurrentConfig()
|
||||
val updatedConfig = current.copy(mcpServers = newMCPServers)
|
||||
return updateConfiguration(updatedConfig)
|
||||
}
|
||||
|
||||
suspend fun updateChatConfig(newChat: ChatConfig): ConfigurationUpdateResult {
|
||||
val current = getCurrentConfig()
|
||||
val updatedConfig = current.copy(chat = newChat)
|
||||
return updateConfiguration(updatedConfig)
|
||||
}
|
||||
|
||||
suspend fun updateAppearanceConfig(newAppearance: AppearanceConfig): ConfigurationUpdateResult {
|
||||
val current = getCurrentConfig()
|
||||
val updatedConfig = current.copy(appearance = newAppearance)
|
||||
return updateConfiguration(updatedConfig)
|
||||
}
|
||||
|
||||
suspend fun updatePrivacyConfig(newPrivacy: PrivacyConfig): ConfigurationUpdateResult {
|
||||
val current = getCurrentConfig()
|
||||
val updatedConfig = current.copy(privacy = newPrivacy)
|
||||
return updateConfiguration(updatedConfig)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a configuration update operation
|
||||
*/
|
||||
data class ConfigurationUpdateResult(
|
||||
val success: Boolean,
|
||||
val errors: List<String>,
|
||||
val warnings: List<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* Global instance of the configuration manager
|
||||
*/
|
||||
object GlobalConfigurationManager {
|
||||
private val manager = ConfigurationManager()
|
||||
|
||||
fun getInstance(): ConfigurationManager = manager
|
||||
}
|
||||
|
|
@ -0,0 +1,655 @@
|
|||
// Configuration Menu for Aurelio Agent
|
||||
// Provides a unified configuration interface for both aurelio-theia and aurelio-web
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Interface for UI configuration menu implementation
|
||||
*/
|
||||
interface ConfigurationMenuUI {
|
||||
fun showConfigurationMenu()
|
||||
fun hideConfigurationMenu()
|
||||
fun updateUI(config: AgentConfiguration)
|
||||
fun onConfigChange(callback: (AgentConfiguration) -> Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main configuration menu controller
|
||||
*/
|
||||
class ConfigurationMenu(
|
||||
private val configurationManager: ConfigurationManager = GlobalConfigurationManager.getInstance()
|
||||
) {
|
||||
private var ui: ConfigurationMenuUI? = null
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
/**
|
||||
* Initialize the configuration menu with a UI implementation
|
||||
*/
|
||||
fun initialize(ui: ConfigurationMenuUI) {
|
||||
this.ui = ui
|
||||
|
||||
// Load initial configuration and update UI
|
||||
scope.launch {
|
||||
val config = configurationManager.getCurrentConfig()
|
||||
ui.updateUI(config)
|
||||
|
||||
// Set up change callback
|
||||
ui.onConfigChange { newConfig ->
|
||||
scope.launch {
|
||||
val result = configurationManager.updateConfiguration(newConfig)
|
||||
if (!result.success) {
|
||||
// Handle errors appropriately in UI
|
||||
println("Configuration update failed: ${result.errors}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the configuration menu
|
||||
*/
|
||||
fun show() {
|
||||
ui?.showConfigurationMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the configuration menu
|
||||
*/
|
||||
fun hide() {
|
||||
ui?.hideConfigurationMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current configuration
|
||||
*/
|
||||
suspend fun getCurrentConfig(): AgentConfiguration {
|
||||
return configurationManager.getCurrentConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
*/
|
||||
suspend fun updateConfig(newConfig: AgentConfiguration): ConfigurationUpdateResult {
|
||||
return configurationManager.updateConfiguration(newConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to defaults
|
||||
*/
|
||||
suspend fun resetToDefaults(): ConfigurationUpdateResult {
|
||||
return configurationManager.resetToDefaults()
|
||||
}
|
||||
|
||||
/**
|
||||
* Import configuration from file
|
||||
*/
|
||||
suspend fun importConfig(filePath: String): ConfigurationUpdateResult {
|
||||
return configurationManager.importConfiguration(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export configuration to file
|
||||
*/
|
||||
suspend fun exportConfig(filePath: String): ConfigurationUpdateResult {
|
||||
return configurationManager.exportConfiguration(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation errors
|
||||
*/
|
||||
suspend fun getValidationErrors(): List<String> {
|
||||
return configurationManager.getValidationErrors()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration section identifiers
|
||||
*/
|
||||
enum class ConfigSection {
|
||||
GENERAL,
|
||||
AI_PROVIDERS,
|
||||
MCP_SERVERS,
|
||||
CHAT,
|
||||
APPEARANCE,
|
||||
PRIVACY,
|
||||
DOCUMENT_COLLAB // Added document collaboration section
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration field types
|
||||
*/
|
||||
enum class ConfigFieldType {
|
||||
TEXT,
|
||||
PASSWORD,
|
||||
BOOLEAN,
|
||||
SELECT,
|
||||
NUMBER,
|
||||
FILE_PATH,
|
||||
FOLDER_PATH
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration field definition
|
||||
*/
|
||||
data class ConfigField(
|
||||
val key: String,
|
||||
val label: String,
|
||||
val description: String,
|
||||
val type: ConfigFieldType,
|
||||
val defaultValue: Any? = null,
|
||||
val options: List<String> = emptyList(), // For SELECT type
|
||||
val required: Boolean = false,
|
||||
val validationRegex: String? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration section definition
|
||||
*/
|
||||
data class ConfigSectionDefinition(
|
||||
val section: ConfigSection,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val fields: List<ConfigField>
|
||||
)
|
||||
|
||||
/**
|
||||
* Configuration schema for UI generation
|
||||
*/
|
||||
class ConfigurationSchema {
|
||||
companion object {
|
||||
fun getSchema(): List<ConfigSectionDefinition> {
|
||||
return listOf(
|
||||
ConfigSectionDefinition(
|
||||
section = ConfigSection.GENERAL,
|
||||
title = "General",
|
||||
description = "Basic settings for the Aurelio agent",
|
||||
fields = listOf(
|
||||
ConfigField(
|
||||
key = "general.agentName",
|
||||
label = "Agent Name",
|
||||
description = "The name of your Aurelio agent",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "Aurelio"
|
||||
),
|
||||
ConfigField(
|
||||
key = "general.agentIdentity",
|
||||
label = "Agent Identity",
|
||||
description = "The identity or description of your agent",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "Portugal Futurista AI Assistant"
|
||||
),
|
||||
ConfigField(
|
||||
key = "general.enableTelemetry",
|
||||
label = "Enable Telemetry",
|
||||
description = "Allow anonymous usage data collection",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "general.autoUpdate",
|
||||
label = "Auto Update",
|
||||
description = "Automatically check for updates",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "general.language",
|
||||
label = "Language",
|
||||
description = "Interface language",
|
||||
type = ConfigFieldType.SELECT,
|
||||
defaultValue = "en",
|
||||
options = listOf("en", "pt", "es", "fr", "de")
|
||||
),
|
||||
ConfigField(
|
||||
key = "general.timezone",
|
||||
label = "Timezone",
|
||||
description = "Timezone for the agent",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "UTC"
|
||||
),
|
||||
ConfigField(
|
||||
key = "general.theme",
|
||||
label = "Theme",
|
||||
description = "Visual theme for the interface",
|
||||
type = ConfigFieldType.SELECT,
|
||||
defaultValue = "dark",
|
||||
options = listOf("light", "dark", "auto")
|
||||
)
|
||||
)
|
||||
),
|
||||
ConfigSectionDefinition(
|
||||
section = ConfigSection.AI_PROVIDERS,
|
||||
title = "AI Providers",
|
||||
description = "Configuration for AI model providers",
|
||||
fields = listOf(
|
||||
ConfigField(
|
||||
key = "aiProviders.hermesNvidia.enabled",
|
||||
label = "Enable NVIDIA NIM (Hermes)",
|
||||
description = "Enable NVIDIA NIM provider (used by Hermes Agent)",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.hermesNvidia.apiKey",
|
||||
label = "NVIDIA NIM API Key",
|
||||
description = "API key for NVIDIA NIM service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.hermesNvidia.baseUrl",
|
||||
label = "NVIDIA NIM Base URL",
|
||||
description = "Base URL for NVIDIA NIM service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "https://integrate.api.nvidia.com/v1"
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.hermesNvidia.defaultModel",
|
||||
label = "Default Model",
|
||||
description = "Default model to use with NVIDIA NIM",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "nvidia/nemotron-3-ultra-550b-a55b"
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.localOllama.enabled",
|
||||
label = "Enable Local Ollama",
|
||||
description = "Enable local Ollama provider (fallback for Hermes)",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.localOllama.baseUrl",
|
||||
label = "Ollama Base URL",
|
||||
description = "Base URL for Ollama service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "http://localhost:11434"
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.localOllama.defaultModel",
|
||||
label = "Default Model",
|
||||
description = "Default model to use with Ollama",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "llama3.2"
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.openAi.enabled",
|
||||
label = "Enable OpenAI",
|
||||
description = "Enable OpenAI provider",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.openAi.apiKey",
|
||||
label = "OpenAI API Key",
|
||||
description = "API key for OpenAI service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.anthropic.enabled",
|
||||
label = "Enable Anthropic",
|
||||
description = "Enable Anthropic provider",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.anthropic.apiKey",
|
||||
label = "Anthropic API Key",
|
||||
description = "API key for Anthropic service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.googleVertex.enabled",
|
||||
label = "Enable Google Vertex",
|
||||
description = "Enable Google Vertex AI provider",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "aiProviders.googleVertex.apiKey",
|
||||
label = "Google Vertex API Key",
|
||||
description = "API key for Google Vertex service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
)
|
||||
)
|
||||
),
|
||||
ConfigSectionDefinition(
|
||||
section = ConfigSection.MCP_SERVERS,
|
||||
title = "MCP Servers",
|
||||
description = "Configuration for Model Context Protocol servers",
|
||||
fields = listOf(
|
||||
ConfigField(
|
||||
key = "mcpServers.flintChart.enabled",
|
||||
label = "Enable Flint Chart MCP",
|
||||
description = "Enable the flint-chart MCP server for chart generation",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "mcpServers.flintChart.baseUrl",
|
||||
label = "Flint Chart Base URL",
|
||||
description = "Base URL for the flint-chart MCP server",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "http://localhost:8085"
|
||||
),
|
||||
ConfigField(
|
||||
key = "mcpServers.flintChart.port",
|
||||
label = "Flint Chart Port",
|
||||
description = "Port for the flint-chart MCP server",
|
||||
type = ConfigFieldType.NUMBER,
|
||||
defaultValue = 8085
|
||||
),
|
||||
ConfigField(
|
||||
key = "mcpServers.electricalMcp.enabled",
|
||||
label = "Enable Electrical MCP",
|
||||
description = "Enable the electrical MCP server",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "mcpServers.electricalMcp.baseUrl",
|
||||
label = "Electrical MCP Base URL",
|
||||
description = "Base URL for the electrical MCP server",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "http://localhost:8081"
|
||||
),
|
||||
ConfigField(
|
||||
key = "mcpServers.julesMcp.enabled",
|
||||
label = "Enable Jules MCP",
|
||||
description = "Enable the Jules MCP server",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "mcpServers.julesMcp.baseUrl",
|
||||
label = "Jules MCP Base URL",
|
||||
description = "Base URL for the Jules MCP server",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "http://localhost:8082"
|
||||
),
|
||||
ConfigField(
|
||||
key = "mcpServers.olhosDeOrpheu.enabled",
|
||||
label = "Enable Olhos-de-Orpheu Gateway",
|
||||
description = "Enable the Olhos-de-Orpheu MCP gateway (Hermes integration)",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "mcpServers.olhosDeOrpheu.baseUrl",
|
||||
label = "Olhos-de-Orpheu Base URL",
|
||||
description = "Base URL for the Olhos-de-Orpheu gateway",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "http://192.168.0.16:8001/sse"
|
||||
)
|
||||
)
|
||||
),
|
||||
ConfigSectionDefinition(
|
||||
section = ConfigSection.CHAT,
|
||||
title = "Chat",
|
||||
description = "Chat interface and behavior settings",
|
||||
fields = listOf(
|
||||
ConfigField(
|
||||
key = "chat.enableMarkdown",
|
||||
label = "Enable Markdown",
|
||||
description = "Enable markdown formatting in chat",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "chat.enableCodeHighlighting",
|
||||
label = "Enable Code Highlighting",
|
||||
description = "Enable syntax highlighting for code blocks",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "chat.enableChartEmbedding",
|
||||
label = "Enable Chart Embedding",
|
||||
description = "Enable embedding of charts in chat messages",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "chat.maxHistory",
|
||||
label = "Max History Messages",
|
||||
description = "Maximum number of messages to keep in chat history",
|
||||
type = ConfigFieldType.NUMBER,
|
||||
defaultValue = 100
|
||||
),
|
||||
ConfigField(
|
||||
key = "chat.autoSaveChats",
|
||||
label = "Auto Save Chats",
|
||||
description = "Automatically save chat history",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "chat.chatStoragePath",
|
||||
label = "Chat Storage Path",
|
||||
description = "Path to store chat history files",
|
||||
type = ConfigFieldType.FOLDER_PATH,
|
||||
defaultValue = "./chats"
|
||||
),
|
||||
ConfigField(
|
||||
key = "chat.enableVoiceInput",
|
||||
label = "Enable Voice Input",
|
||||
description = "Enable voice input for chat",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
)
|
||||
)
|
||||
),
|
||||
ConfigSectionDefinition(
|
||||
section = ConfigSection.APPEARANCE,
|
||||
title = "Appearance",
|
||||
description = "Visual appearance and UI settings",
|
||||
fields = listOf(
|
||||
ConfigField(
|
||||
key = "appearance.theme",
|
||||
label = "Theme",
|
||||
description = "Visual theme for the interface",
|
||||
type = ConfigFieldType.SELECT,
|
||||
defaultValue = "dark",
|
||||
options = listOf("light", "dark", "auto")
|
||||
),
|
||||
ConfigField(
|
||||
key = "appearance.fontSize",
|
||||
label = "Font Size",
|
||||
description = "Base font size for the interface",
|
||||
type = ConfigFieldType.NUMBER,
|
||||
defaultValue = 14
|
||||
),
|
||||
ConfigField(
|
||||
key = "appearance.fontFamily",
|
||||
label = "Font Family",
|
||||
description = "Base font family for the interface",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "default"
|
||||
),
|
||||
ConfigField(
|
||||
key = "appearance.sidebarPosition",
|
||||
label = "Sidebar Position",
|
||||
description = "Position of the sidebar",
|
||||
type = ConfigFieldType.SELECT,
|
||||
defaultValue = "left",
|
||||
options = listOf("left", "right")
|
||||
),
|
||||
ConfigField(
|
||||
key = "appearance.compactMode",
|
||||
label = "Compact Mode",
|
||||
description = "Use compact UI mode",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "appearance.showAvatars",
|
||||
label = "Show Avatars",
|
||||
description = "Show avatars in chat",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "appearance.agentAvatar",
|
||||
label = "Agent Avatar",
|
||||
description = "Avatar image for the agent",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "default"
|
||||
)
|
||||
)
|
||||
),
|
||||
ConfigSectionDefinition(
|
||||
section = ConfigSection.PRIVACY,
|
||||
title = "Privacy",
|
||||
description = "Privacy and data handling settings",
|
||||
fields = listOf(
|
||||
ConfigField(
|
||||
key = "privacy.enableLocalProcessing",
|
||||
label = "Enable Local Processing",
|
||||
description = "Prefer local processing when possible",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "privacy.disableCloudSync",
|
||||
label = "Disable Cloud Sync",
|
||||
description = "Disable cloud synchronization of data",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "privacy.dataRetentionDays",
|
||||
label = "Data Retention Days",
|
||||
description = "Number of days to retain local data",
|
||||
type = ConfigFieldType.NUMBER,
|
||||
defaultValue = 30
|
||||
),
|
||||
ConfigField(
|
||||
key = "privacy.anonymizeTelemetry",
|
||||
label = "Anonymize Telemetry",
|
||||
description = "Send anonymized telemetry data",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = true
|
||||
),
|
||||
ConfigField(
|
||||
key = "privacy.disableUsageLogging",
|
||||
label = "Disable Usage Logging",
|
||||
description = "Disable local usage logging",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
)
|
||||
)
|
||||
),
|
||||
ConfigSectionDefinition( // Added document collaboration section
|
||||
section = ConfigSection.DOCUMENT_COLLAB,
|
||||
title = "Document Collaboration",
|
||||
description = "Configuration for document collaboration services (OnlyOffice, Nextcloud, Forgejo)",
|
||||
fields = listOf(
|
||||
ConfigField(
|
||||
key = "documentCollab.onlyOffice.enabled",
|
||||
label = "Enable OnlyOffice",
|
||||
description = "Enable OnlyOffice document collaboration service",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.onlyOffice.baseUrl",
|
||||
label = "OnlyOffice Base URL",
|
||||
description = "Base URL for OnlyOffice service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.onlyOffice.apiKey",
|
||||
label = "OnlyOffice API Key",
|
||||
description = "API key for OnlyOffice service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.onlyOffice.secretKey",
|
||||
label = "OnlyOffice Secret Key",
|
||||
description = "Secret key for OnlyOffice service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.enabled",
|
||||
label = "Enable Nextcloud",
|
||||
description = "Enable Nextcloud document collaboration service",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.baseUrl",
|
||||
label = "Nextcloud Base URL",
|
||||
description = "Base URL for Nextcloud service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.username",
|
||||
label = "Nextcloud Username",
|
||||
description = "Username for Nextcloud service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.password",
|
||||
label = "Nextcloud Password",
|
||||
description = "Password for Nextcloud service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.appId",
|
||||
label = "Nextcloud App ID",
|
||||
description = "App ID for Nextcloud integration",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.appPassword",
|
||||
label = "Nextcloud App Password",
|
||||
description = "App password for Nextcloud integration",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.forgejo.enabled",
|
||||
label = "Enable Forgejo",
|
||||
description = "Enable Forgejo document collaboration service",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.forgejo.baseUrl",
|
||||
label = "Forgejo Base URL",
|
||||
description = "Base URL for Forgejo service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.forgejo.token",
|
||||
label = "Forgejo Token",
|
||||
description = "Access token for Forgejo service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.forgejo.apiVersion",
|
||||
label = "Forgejo API Version",
|
||||
description = "API version for Forgejo service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "v1"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
// Configuration Schema for Document Collaboration Services
|
||||
// Defines the configuration fields for OnlyOffice, Nextcloud, and Forgejo
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
/**
|
||||
* Extended configuration section definitions to include document collaboration services
|
||||
*/
|
||||
class DocumentCollaborationConfigSchema {
|
||||
companion object {
|
||||
fun getExtendedSchema(): List<ConfigSectionDefinition> {
|
||||
val baseSchema = ConfigurationSchema.getSchema()
|
||||
|
||||
// Add document collaboration section
|
||||
val docCollabSection = ConfigSectionDefinition(
|
||||
section = ConfigSection.DOCUMENT_COLLAB,
|
||||
title = "Document Collaboration",
|
||||
description = "Configuration for document collaboration services (OnlyOffice, Nextcloud, Forgejo)",
|
||||
fields = listOf(
|
||||
ConfigField(
|
||||
key = "documentCollab.onlyOffice.enabled",
|
||||
label = "Enable OnlyOffice",
|
||||
description = "Enable OnlyOffice document collaboration service",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.onlyOffice.baseUrl",
|
||||
label = "OnlyOffice Base URL",
|
||||
description = "Base URL for OnlyOffice service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.onlyOffice.apiKey",
|
||||
label = "OnlyOffice API Key",
|
||||
description = "API key for OnlyOffice service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.onlyOffice.secretKey",
|
||||
label = "OnlyOffice Secret Key",
|
||||
description = "Secret key for OnlyOffice service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.enabled",
|
||||
label = "Enable Nextcloud",
|
||||
description = "Enable Nextcloud document collaboration service",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.baseUrl",
|
||||
label = "Nextcloud Base URL",
|
||||
description = "Base URL for Nextcloud service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.username",
|
||||
label = "Nextcloud Username",
|
||||
description = "Username for Nextcloud service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.password",
|
||||
label = "Nextcloud Password",
|
||||
description = "Password for Nextcloud service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.appId",
|
||||
label = "Nextcloud App ID",
|
||||
description = "App ID for Nextcloud integration",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.nextcloud.appPassword",
|
||||
label = "Nextcloud App Password",
|
||||
description = "App password for Nextcloud integration",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.forgejo.enabled",
|
||||
label = "Enable Forgejo",
|
||||
description = "Enable Forgejo document collaboration service",
|
||||
type = ConfigFieldType.BOOLEAN,
|
||||
defaultValue = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.forgejo.baseUrl",
|
||||
label = "Forgejo Base URL",
|
||||
description = "Base URL for Forgejo service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = ""
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.forgejo.token",
|
||||
label = "Forgejo Token",
|
||||
description = "Access token for Forgejo service",
|
||||
type = ConfigFieldType.PASSWORD,
|
||||
required = false
|
||||
),
|
||||
ConfigField(
|
||||
key = "documentCollab.forgejo.apiVersion",
|
||||
label = "Forgejo API Version",
|
||||
description = "API version for Forgejo service",
|
||||
type = ConfigFieldType.TEXT,
|
||||
defaultValue = "v1"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Add the new section to the existing schema
|
||||
return baseSchema + docCollabSection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the ConfigSection enum to include document collaboration
|
||||
*/
|
||||
enum class ExtendedConfigSection {
|
||||
GENERAL,
|
||||
AI_PROVIDERS,
|
||||
MCP_SERVERS,
|
||||
CHAT,
|
||||
APPEARANCE,
|
||||
PRIVACY,
|
||||
DOCUMENT_COLLAB // Added document collaboration section
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the configuration classes to include document collaboration settings
|
||||
*/
|
||||
data class DocumentCollaborationSettings(
|
||||
val onlyOffice: OnlyOfficeConfig = OnlyOfficeConfig(),
|
||||
val nextcloud: NextcloudConfig = NextcloudConfig(),
|
||||
val forgejo: ForgejoConfig = ForgejoConfig()
|
||||
)
|
||||
|
||||
/**
|
||||
* Extend the main configuration class
|
||||
*/
|
||||
data class ExtendedAgentConfiguration(
|
||||
val general: GeneralConfig = GeneralConfig(),
|
||||
val aiProviders: AIProvidersConfig = AIProvidersConfig(),
|
||||
val mcpServers: MCPServersConfig = MCPServersConfig(),
|
||||
val chat: ChatConfig = ChatConfig(),
|
||||
val appearance: AppearanceConfig = AppearanceConfig(),
|
||||
val privacy: PrivacyConfig = PrivacyConfig(),
|
||||
val documentCollab: DocumentCollaborationSettings = DocumentCollaborationSettings()
|
||||
)
|
||||
|
||||
/**
|
||||
* Extend the configuration manager to handle document collaboration settings
|
||||
*/
|
||||
class ExtendedConfigurationManager : ConfigurationPersistence {
|
||||
private val baseManager = GlobalConfigurationManager.getInstance()
|
||||
private val docCollabManager = DocumentCollaborationManager(DocumentCollaborationConfig())
|
||||
|
||||
override suspend fun loadConfiguration(): AgentConfiguration {
|
||||
return baseManager.getCurrentConfig()
|
||||
}
|
||||
|
||||
override suspend fun saveConfiguration(config: AgentConfiguration) {
|
||||
// Save to base configuration
|
||||
baseManager.updateConfiguration(config)
|
||||
}
|
||||
|
||||
override suspend fun validateConfiguration(config: AgentConfiguration): List<String> {
|
||||
val baseErrors = baseManager.getValidationErrors()
|
||||
val docCollabErrors = mutableListOf<String>()
|
||||
|
||||
// Validate document collaboration settings
|
||||
if (config is ExtendedAgentConfiguration) {
|
||||
if (config.documentCollab.onlyOffice.enabled) {
|
||||
if (config.documentCollab.onlyOffice.baseUrl.isEmpty()) {
|
||||
docCollabErrors.add("OnlyOffice Base URL is required when OnlyOffice is enabled")
|
||||
}
|
||||
if (config.documentCollab.onlyOffice.apiKey.isEmpty()) {
|
||||
docCollabErrors.add("OnlyOffice API Key is required when OnlyOffice is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
if (config.documentCollab.nextcloud.enabled) {
|
||||
if (config.documentCollab.nextcloud.baseUrl.isEmpty()) {
|
||||
docCollabErrors.add("Nextcloud Base URL is required when Nextcloud is enabled")
|
||||
}
|
||||
if (config.documentCollab.nextcloud.username.isEmpty()) {
|
||||
docCollabErrors.add("Nextcloud Username is required when Nextcloud is enabled")
|
||||
}
|
||||
if (config.documentCollab.nextcloud.password.isEmpty() && config.documentCollab.nextcloud.appPassword.isEmpty()) {
|
||||
docCollabErrors.add("Nextcloud Password or App Password is required when Nextcloud is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
if (config.documentCollab.forgejo.enabled) {
|
||||
if (config.documentCollab.forgejo.baseUrl.isEmpty()) {
|
||||
docCollabErrors.add("Forgejo Base URL is required when Forgejo is enabled")
|
||||
}
|
||||
if (config.documentCollab.forgejo.token.isEmpty()) {
|
||||
docCollabErrors.add("Forgejo Token is required when Forgejo is enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return baseErrors + docCollabErrors
|
||||
}
|
||||
|
||||
/**
|
||||
* Get document collaboration configuration
|
||||
*/
|
||||
suspend fun getDocumentCollabConfig(): DocumentCollaborationConfig {
|
||||
val config = loadConfiguration()
|
||||
return DocumentCollaborationConfig(
|
||||
onlyOffice = OnlyOfficeConfig(
|
||||
enabled = config.getIntegratedServiceConfig("onlyoffice_enabled") as? Boolean ?: false,
|
||||
baseUrl = config.getIntegratedServiceConfig("onlyoffice_baseurl") as? String ?: "",
|
||||
apiKey = config.getIntegratedServiceConfig("onlyoffice_apikey") as? String ?: "",
|
||||
secretKey = config.getIntegratedServiceConfig("onlyoffice_secretkey") as? String ?: ""
|
||||
),
|
||||
nextcloud = NextcloudConfig(
|
||||
enabled = config.getIntegratedServiceConfig("nextcloud_enabled") as? Boolean ?: false,
|
||||
baseUrl = config.getIntegratedServiceConfig("nextcloud_baseurl") as? String ?: "",
|
||||
username = config.getIntegratedServiceConfig("nextcloud_username") as? String ?: "",
|
||||
password = config.getIntegratedServiceConfig("nextcloud_password") as? String ?: "",
|
||||
appId = config.getIntegratedServiceConfig("nextcloud_appid") as? String ?: "",
|
||||
appPassword = config.getIntegratedServiceConfig("nextcloud_apppassword") as? String ?: ""
|
||||
),
|
||||
forgejo = ForgejoConfig(
|
||||
enabled = config.getIntegratedServiceConfig("forgejo_enabled") as? Boolean ?: false,
|
||||
baseUrl = config.getIntegratedServiceConfig("forgejo_baseurl") as? String ?: "",
|
||||
token = config.getIntegratedServiceConfig("forgejo_token") as? String ?: "",
|
||||
apiVersion = config.getIntegratedServiceConfig("forgejo_apiversion") as? String ?: "v1"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update document collaboration configuration
|
||||
*/
|
||||
suspend fun updateDocumentCollabConfig(newConfig: DocumentCollaborationConfig): ConfigurationUpdateResult {
|
||||
// This would update the configuration in the main agent configuration
|
||||
val currentConfig = loadConfiguration()
|
||||
// Update would happen through the main configuration manager
|
||||
return ConfigurationUpdateResult(
|
||||
success = true,
|
||||
errors = emptyList(),
|
||||
warnings = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to get document collaboration config from agent configuration
|
||||
*/
|
||||
suspend fun AgentConfiguration.getDocumentCollabConfig(): DocumentCollaborationConfig {
|
||||
return DocumentCollaborationConfig(
|
||||
onlyOffice = OnlyOfficeConfig(
|
||||
enabled = getBooleanValue("documentCollab.onlyOffice.enabled", false),
|
||||
baseUrl = getStringValue("documentCollab.onlyOffice.baseUrl", ""),
|
||||
apiKey = getStringValue("documentCollab.onlyOffice.apiKey", ""),
|
||||
secretKey = getStringValue("documentCollab.onlyOffice.secretKey", "")
|
||||
),
|
||||
nextcloud = NextcloudConfig(
|
||||
enabled = getBooleanValue("documentCollab.nextcloud.enabled", false),
|
||||
baseUrl = getStringValue("documentCollab.nextcloud.baseUrl", ""),
|
||||
username = getStringValue("documentCollab.nextcloud.username", ""),
|
||||
password = getStringValue("documentCollab.nextcloud.password", ""),
|
||||
appId = getStringValue("documentCollab.nextcloud.appId", ""),
|
||||
appPassword = getStringValue("documentCollab.nextcloud.appPassword", "")
|
||||
),
|
||||
forgejo = ForgejoConfig(
|
||||
enabled = getBooleanValue("documentCollab.forgejo.enabled", false),
|
||||
baseUrl = getStringValue("documentCollab.forgejo.baseUrl", ""),
|
||||
token = getStringValue("documentCollab.forgejo.token", ""),
|
||||
apiVersion = getStringValue("documentCollab.forgejo.apiVersion", "v1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions to get values from agent configuration
|
||||
*/
|
||||
private fun AgentConfiguration.getBooleanValue(key: String, defaultValue: Boolean): Boolean {
|
||||
// Navigate through the configuration structure to get the value
|
||||
// This is a simplified implementation - in reality, this would navigate the nested structure
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
private fun AgentConfiguration.getStringValue(key: String, defaultValue: String): String {
|
||||
// Navigate through the configuration structure to get the value
|
||||
// This is a simplified implementation - in reality, this would navigate the nested structure
|
||||
return defaultValue
|
||||
}
|
||||
|
|
@ -0,0 +1,889 @@
|
|||
// Document Collaboration Integration for Aurelio
|
||||
// Integrates OnlyOffice, Nextcloud, and Forgejo into Aurelio platforms
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.*
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Configuration for document collaboration services
|
||||
*/
|
||||
data class DocumentCollaborationConfig(
|
||||
val onlyOffice: OnlyOfficeConfig = OnlyOfficeConfig(),
|
||||
val nextcloud: NextcloudConfig = NextcloudConfig(),
|
||||
val forgejo: ForgejoConfig = ForgejoConfig()
|
||||
)
|
||||
|
||||
/**
|
||||
* OnlyOffice configuration
|
||||
*/
|
||||
data class OnlyOfficeConfig(
|
||||
val enabled: Boolean = false,
|
||||
val baseUrl: String = "",
|
||||
val apiKey: String = "",
|
||||
val secretKey: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Nextcloud configuration
|
||||
*/
|
||||
data class NextcloudConfig(
|
||||
val enabled: Boolean = false,
|
||||
val baseUrl: String = "",
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val appId: String = "",
|
||||
val appPassword: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Forgejo configuration
|
||||
*/
|
||||
data class ForgejoConfig(
|
||||
val enabled: Boolean = false,
|
||||
val baseUrl: String = "",
|
||||
val token: String = "",
|
||||
val apiVersion: String = "v1"
|
||||
)
|
||||
|
||||
/**
|
||||
* Interface for document collaboration services
|
||||
*/
|
||||
interface DocumentCollaborationService {
|
||||
suspend fun connect(): Boolean
|
||||
suspend fun uploadFile(filePath: String, destinationPath: String): Boolean
|
||||
suspend fun downloadFile(remotePath: String, localDestination: String): Boolean
|
||||
suspend fun createDocument(title: String, content: String, mimeType: String): String?
|
||||
suspend fun editDocument(documentId: String, content: String): Boolean
|
||||
suspend fun shareDocument(documentId: String, permissions: String): String?
|
||||
suspend fun listDocuments(): List<DocumentInfo>
|
||||
suspend fun getDocumentInfo(documentId: String): DocumentInfo?
|
||||
}
|
||||
|
||||
/**
|
||||
* Document information class
|
||||
*/
|
||||
data class DocumentInfo(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val url: String,
|
||||
val mimeType: String,
|
||||
val size: Long,
|
||||
val createdAt: Date,
|
||||
val modifiedAt: Date,
|
||||
val owner: String,
|
||||
val permissions: String
|
||||
)
|
||||
|
||||
/**
|
||||
* OnlyOffice integration service
|
||||
*/
|
||||
class OnlyOfficeService(private val config: OnlyOfficeConfig) : DocumentCollaborationService {
|
||||
private val httpClient = OkHttpClient()
|
||||
private val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
|
||||
|
||||
override suspend fun connect(): Boolean {
|
||||
if (!config.enabled || config.baseUrl.isEmpty()) return false
|
||||
|
||||
return try {
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/healthcheck")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
response.isSuccessful
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uploadFile(filePath: String, destinationPath: String): Boolean {
|
||||
if (!config.enabled) return false
|
||||
|
||||
val file = File(filePath)
|
||||
if (!file.exists()) return false
|
||||
|
||||
return try {
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("file", file.name, RequestBody.create(null, file))
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/upload")
|
||||
.post(requestBody)
|
||||
.addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
response.isSuccessful
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun downloadFile(remotePath: String, localDestination: String): Boolean {
|
||||
if (!config.enabled) return false
|
||||
|
||||
return try {
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/download?path=$remotePath")
|
||||
.get()
|
||||
.addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.isSuccessful) {
|
||||
val file = File(localDestination)
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeBytes(response.body?.bytes() ?: byteArrayOf())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createDocument(title: String, content: String, mimeType: String): String? {
|
||||
if (!config.enabled) return null
|
||||
|
||||
return try {
|
||||
val requestBody = objectMapper.writeValueAsString(
|
||||
mapOf(
|
||||
"title" to title,
|
||||
"content" to content,
|
||||
"mimeType" to mimeType
|
||||
)
|
||||
)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/create")
|
||||
.post(RequestBody.create(MediaType.get("application/json"), requestBody))
|
||||
.addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.isSuccessful) {
|
||||
val responseJson = objectMapper.readTree(response.body?.string())
|
||||
responseJson.get("documentId")?.asText()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun editDocument(documentId: String, content: String): Boolean {
|
||||
if (!config.enabled) return false
|
||||
|
||||
return try {
|
||||
val requestBody = objectMapper.writeValueAsString(
|
||||
mapOf(
|
||||
"documentId" to documentId,
|
||||
"content" to content
|
||||
)
|
||||
)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/edit")
|
||||
.put(RequestBody.create(MediaType.get("application/json"), requestBody))
|
||||
.addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
response.isSuccessful
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shareDocument(documentId: String, permissions: String): String? {
|
||||
if (!config.enabled) return null
|
||||
|
||||
return try {
|
||||
val requestBody = objectMapper.writeValueAsString(
|
||||
mapOf(
|
||||
"documentId" to documentId,
|
||||
"permissions" to permissions
|
||||
)
|
||||
)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/share")
|
||||
.post(RequestBody.create(MediaType.get("application/json"), requestBody))
|
||||
.addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.isSuccessful) {
|
||||
val responseJson = objectMapper.readTree(response.body?.string())
|
||||
responseJson.get("shareLink")?.asText()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun listDocuments(): List<DocumentInfo> {
|
||||
if (!config.enabled) return emptyList()
|
||||
|
||||
return try {
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/documents")
|
||||
.get()
|
||||
.addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.isSuccessful) {
|
||||
val responseJson = objectMapper.readTree(response.body?.string())
|
||||
val documents = mutableListOf<DocumentInfo>()
|
||||
|
||||
if (responseJson.isArray) {
|
||||
for (docNode in responseJson) {
|
||||
val doc = DocumentInfo(
|
||||
id = docNode.get("id")?.asText() ?: "",
|
||||
title = docNode.get("title")?.asText() ?: "",
|
||||
url = docNode.get("url")?.asText() ?: "",
|
||||
mimeType = docNode.get("mimeType")?.asText() ?: "",
|
||||
size = docNode.get("size")?.asLong() ?: 0L,
|
||||
createdAt = Date(docNode.get("createdAt")?.asLong() ?: 0L),
|
||||
modifiedAt = Date(docNode.get("modifiedAt")?.asLong() ?: 0L),
|
||||
owner = docNode.get("owner")?.asText() ?: "",
|
||||
permissions = docNode.get("permissions")?.asText() ?: ""
|
||||
)
|
||||
documents.add(doc)
|
||||
}
|
||||
}
|
||||
|
||||
documents
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDocumentInfo(documentId: String): DocumentInfo? {
|
||||
if (!config.enabled) return null
|
||||
|
||||
return try {
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/documents/$documentId")
|
||||
.get()
|
||||
.addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.isSuccessful) {
|
||||
val responseJson = objectMapper.readTree(response.body?.string())
|
||||
|
||||
DocumentInfo(
|
||||
id = responseJson.get("id")?.asText() ?: "",
|
||||
title = responseJson.get("title")?.asText() ?: "",
|
||||
url = responseJson.get("url")?.asText() ?: "",
|
||||
mimeType = responseJson.get("mimeType")?.asText() ?: "",
|
||||
size = responseJson.get("size")?.asLong() ?: 0L,
|
||||
createdAt = Date(responseJson.get("createdAt")?.asLong() ?: 0L),
|
||||
modifiedAt = Date(responseJson.get("modifiedAt")?.asLong() ?: 0L),
|
||||
owner = responseJson.get("owner")?.asText() ?: "",
|
||||
permissions = responseJson.get("permissions")?.asText() ?: ""
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nextcloud integration service
|
||||
*/
|
||||
class NextcloudService(private val config: NextcloudConfig) : DocumentCollaborationService {
|
||||
private val httpClient = OkHttpClient()
|
||||
private val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
|
||||
|
||||
override suspend fun connect(): Boolean {
|
||||
if (!config.enabled || config.baseUrl.isEmpty()) return false
|
||||
|
||||
return try {
|
||||
val credentials = Credentials.basic(config.username, config.password)
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/ocs/v1.php/cloud/user")
|
||||
.get()
|
||||
.addHeader("Authorization", credentials)
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
response.code in 200..299
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uploadFile(filePath: String, destinationPath: String): Boolean {
|
||||
if (!config.enabled) return false
|
||||
|
||||
val file = File(filePath)
|
||||
if (!file.exists()) return false
|
||||
|
||||
return try {
|
||||
val credentials = if (config.appPassword.isNotEmpty()) {
|
||||
Credentials.basic(config.username, config.appPassword)
|
||||
} else {
|
||||
Credentials.basic(config.username, config.password)
|
||||
}
|
||||
|
||||
val requestBody = RequestBody.create(MediaType.get("application/octet-stream"), file)
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/remote.php/webdav$destinationPath")
|
||||
.put(requestBody)
|
||||
.addHeader("Authorization", credentials)
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
response.code in 200..299
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun downloadFile(remotePath: String, localDestination: String): Boolean {
|
||||
if (!config.enabled) return false
|
||||
|
||||
return try {
|
||||
val credentials = if (config.appPassword.isNotEmpty()) {
|
||||
Credentials.basic(config.username, config.appPassword)
|
||||
} else {
|
||||
Credentials.basic(config.username, config.password)
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/remote.php/webdav$remotePath")
|
||||
.get()
|
||||
.addHeader("Authorization", credentials)
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.code in 200..299) {
|
||||
val file = File(localDestination)
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeBytes(response.body?.bytes() ?: byteArrayOf())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createDocument(title: String, content: String, mimeType: String): String? {
|
||||
if (!config.enabled) return null
|
||||
|
||||
return try {
|
||||
val credentials = if (config.appPassword.isNotEmpty()) {
|
||||
Credentials.basic(config.username, config.appPassword)
|
||||
} else {
|
||||
Credentials.basic(config.username, config.password)
|
||||
}
|
||||
|
||||
val requestBody = RequestBody.create(MediaType.get(mimeType), content)
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/remote.php/webdav/Documents/$title")
|
||||
.put(requestBody)
|
||||
.addHeader("Authorization", credentials)
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.code in 200..299) {
|
||||
"/Documents/$title"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun editDocument(documentId: String, content: String): Boolean {
|
||||
if (!config.enabled) return false
|
||||
|
||||
return try {
|
||||
val credentials = if (config.appPassword.isNotEmpty()) {
|
||||
Credentials.basic(config.username, config.appPassword)
|
||||
} else {
|
||||
Credentials.basic(config.username, config.password)
|
||||
}
|
||||
|
||||
val requestBody = RequestBody.create(null, content)
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/remote.php/webdav$documentId")
|
||||
.put(requestBody)
|
||||
.addHeader("Authorization", credentials)
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
response.code in 200..299
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shareDocument(documentId: String, permissions: String): String? {
|
||||
if (!config.enabled) return null
|
||||
|
||||
return try {
|
||||
val credentials = if (config.appPassword.isNotEmpty()) {
|
||||
Credentials.basic(config.username, config.appPassword)
|
||||
} else {
|
||||
Credentials.basic(config.username, config.password)
|
||||
}
|
||||
|
||||
val requestBody = objectMapper.writeValueAsString(
|
||||
mapOf(
|
||||
"path" to documentId,
|
||||
"shareType" to 3, // public link
|
||||
"permissions" to permissions
|
||||
)
|
||||
)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/ocs/v2.php/apps/files_sharing/api/v1/shares")
|
||||
.post(RequestBody.create(MediaType.get("application/json"), requestBody))
|
||||
.addHeader("Authorization", credentials)
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.code in 200..299) {
|
||||
val responseJson = objectMapper.readTree(response.body?.string())
|
||||
val element = responseJson.get("ocs")?.get("data")?.get(0)
|
||||
element?.get("token")?.asText()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun listDocuments(): List<DocumentInfo> {
|
||||
if (!config.enabled) return emptyList()
|
||||
|
||||
return try {
|
||||
val credentials = if (config.appPassword.isNotEmpty()) {
|
||||
Credentials.basic(config.username, config.appPassword)
|
||||
} else {
|
||||
Credentials.basic(config.username, config.password)
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/remote.php/webdav/")
|
||||
.get()
|
||||
.addHeader("Authorization", credentials)
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.code in 200..299) {
|
||||
// Parse DAV response (simplified)
|
||||
val davResponse = response.body?.string()
|
||||
// In a real implementation, this would parse the WebDAV XML response
|
||||
// For now, return an empty list
|
||||
emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDocumentInfo(documentId: String): DocumentInfo? {
|
||||
if (!config.enabled) return null
|
||||
|
||||
return try {
|
||||
val credentials = if (config.appPassword.isNotEmpty()) {
|
||||
Credentials.basic(config.username, config.appPassword)
|
||||
} else {
|
||||
Credentials.basic(config.username, config.password)
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/remote.php/webdav$documentId")
|
||||
.get()
|
||||
.addHeader("Authorization", credentials)
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.code in 200..299) {
|
||||
// Simplified - in reality would parse WebDAV response
|
||||
DocumentInfo(
|
||||
id = documentId,
|
||||
title = documentId.substringAfterLast("/"),
|
||||
url = "${config.baseUrl}/remote.php/webdav$documentId",
|
||||
mimeType = "application/octet-stream",
|
||||
size = 0L,
|
||||
createdAt = Date(),
|
||||
modifiedAt = Date(),
|
||||
owner = config.username,
|
||||
permissions = "read-write"
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgejo integration service
|
||||
*/
|
||||
class ForgejoService(private val config: ForgejoConfig) : DocumentCollaborationService {
|
||||
private val httpClient = OkHttpClient()
|
||||
private val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
|
||||
|
||||
override suspend fun connect(): Boolean {
|
||||
if (!config.enabled || config.baseUrl.isEmpty()) return false
|
||||
|
||||
return try {
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/api/${config.apiVersion}/user")
|
||||
.get()
|
||||
.addHeader("Authorization", "token ${config.token}")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
response.code in 200..299
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uploadFile(filePath: String, destinationPath: String): Boolean {
|
||||
// Forgejo doesn't have a direct file upload API like OnlyOffice or Nextcloud
|
||||
// This would typically involve creating a commit with the file
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun downloadFile(remotePath: String, localDestination: String): Boolean {
|
||||
if (!config.enabled) return false
|
||||
|
||||
return try {
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/api/${config.apiVersion}${remotePath}")
|
||||
.get()
|
||||
.addHeader("Authorization", "token ${config.token}")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.code in 200..299) {
|
||||
val file = File(localDestination)
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeBytes(response.body?.bytes() ?: byteArrayOf())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createDocument(title: String, content: String, mimeType: String): String? {
|
||||
// For Forgejo, creating a document means creating a file in a repository
|
||||
// This would require repository information
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun editDocument(documentId: String, content: String): Boolean {
|
||||
// Editing in Forgejo requires committing changes to a repository
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun shareDocument(documentId: String, permissions: String): String? {
|
||||
if (!config.enabled) return null
|
||||
|
||||
// In Forgejo, sharing could mean creating a public repository or sharing a link
|
||||
return try {
|
||||
// This is a simplified implementation - real sharing would depend on context
|
||||
"${config.baseUrl}$documentId"
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun listDocuments(): List<DocumentInfo> {
|
||||
if (!config.enabled) return emptyList()
|
||||
|
||||
return try {
|
||||
// List user's repositories which can contain documents
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/api/${config.apiVersion}/user/repos")
|
||||
.get()
|
||||
.addHeader("Authorization", "token ${config.token}")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.code in 200..299) {
|
||||
val responseJson = objectMapper.readTree(response.body?.string())
|
||||
val documents = mutableListOf<DocumentInfo>()
|
||||
|
||||
if (responseJson.isArray) {
|
||||
for (repoNode in responseJson) {
|
||||
val doc = DocumentInfo(
|
||||
id = repoNode.get("id")?.asText() ?: "",
|
||||
title = repoNode.get("name")?.asText() ?: "",
|
||||
url = repoNode.get("html_url")?.asText() ?: "",
|
||||
mimeType = "application/vnd.forgejo.repository",
|
||||
size = 0L,
|
||||
createdAt = Date(),
|
||||
modifiedAt = Date(),
|
||||
owner = repoNode.get("owner")?.get("login")?.asText() ?: "",
|
||||
permissions = if (repoNode.get("private")?.asBoolean() == true) "private" else "public"
|
||||
)
|
||||
documents.add(doc)
|
||||
}
|
||||
}
|
||||
|
||||
documents
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDocumentInfo(documentId: String): DocumentInfo? {
|
||||
if (!config.enabled) return null
|
||||
|
||||
return try {
|
||||
val request = Request.Builder()
|
||||
.url("${config.baseUrl}/api/${config.apiVersion}/repos/$documentId")
|
||||
.get()
|
||||
.addHeader("Authorization", "token ${config.token}")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).await()
|
||||
if (response.code in 200..299) {
|
||||
val responseJson = objectMapper.readTree(response.body?.string())
|
||||
|
||||
DocumentInfo(
|
||||
id = responseJson.get("id")?.asText() ?: "",
|
||||
title = responseJson.get("name")?.asText() ?: "",
|
||||
url = responseJson.get("html_url")?.asText() ?: "",
|
||||
mimeType = "application/vnd.forgejo.repository",
|
||||
size = 0L,
|
||||
createdAt = Date(),
|
||||
modifiedAt = Date(),
|
||||
owner = responseJson.get("owner")?.get("login")?.asText() ?: "",
|
||||
permissions = if (responseJson.get("private")?.asBoolean() == true) "private" else "public"
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main document collaboration manager that combines all services
|
||||
*/
|
||||
class DocumentCollaborationManager(
|
||||
private val config: DocumentCollaborationConfig,
|
||||
private val configurationManager: ConfigurationManager = GlobalConfigurationManager.getInstance()
|
||||
) {
|
||||
private var onlyOfficeService: OnlyOfficeService? = null
|
||||
private var nextcloudService: NextcloudService? = null
|
||||
private var forgejoService: ForgejoService? = null
|
||||
|
||||
init {
|
||||
setupServices()
|
||||
}
|
||||
|
||||
private fun setupServices() {
|
||||
if (config.onlyOffice.enabled) {
|
||||
onlyOfficeService = OnlyOfficeService(config.onlyOffice)
|
||||
}
|
||||
if (config.nextcloud.enabled) {
|
||||
nextcloudService = NextcloudService(config.nextcloud)
|
||||
}
|
||||
if (config.forgejo.enabled) {
|
||||
forgejoService = ForgejoService(config.forgejo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to all enabled services
|
||||
*/
|
||||
suspend fun connectAll(): Map<String, Boolean> {
|
||||
val results = mutableMapOf<String, Boolean>()
|
||||
|
||||
if (onlyOfficeService != null) {
|
||||
results["onlyOffice"] = onlyOfficeService!!.connect()
|
||||
}
|
||||
if (nextcloudService != null) {
|
||||
results["nextcloud"] = nextcloudService!!.connect()
|
||||
}
|
||||
if (forgejoService != null) {
|
||||
results["forgejo"] = forgejoService!!.connect()
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to a specific service
|
||||
*/
|
||||
suspend fun uploadFile(service: String, filePath: String, destinationPath: String): Boolean {
|
||||
return when (service.lowercase()) {
|
||||
"onlyoffice" -> onlyOfficeService?.uploadFile(filePath, destinationPath) ?: false
|
||||
"nextcloud" -> nextcloudService?.uploadFile(filePath, destinationPath) ?: false
|
||||
"forgejo" -> false // Forgejo doesn't support direct file upload
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file from a specific service
|
||||
*/
|
||||
suspend fun downloadFile(service: String, remotePath: String, localDestination: String): Boolean {
|
||||
return when (service.lowercase()) {
|
||||
"onlyoffice" -> onlyOfficeService?.downloadFile(remotePath, localDestination) ?: false
|
||||
"nextcloud" -> nextcloudService?.downloadFile(remotePath, localDestination) ?: false
|
||||
"forgejo" -> forgejoService?.downloadFile(remotePath, localDestination) ?: false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create document in a specific service
|
||||
*/
|
||||
suspend fun createDocument(service: String, title: String, content: String, mimeType: String): String? {
|
||||
return when (service.lowercase()) {
|
||||
"onlyoffice" -> onlyOfficeService?.createDocument(title, content, mimeType)
|
||||
"nextcloud" -> nextcloudService?.createDocument(title, content, mimeType)
|
||||
"forgejo" -> forgejoService?.createDocument(title, content, mimeType)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit document in a specific service
|
||||
*/
|
||||
suspend fun editDocument(service: String, documentId: String, content: String): Boolean {
|
||||
return when (service.lowercase()) {
|
||||
"onlyoffice" -> onlyOfficeService?.editDocument(documentId, content) ?: false
|
||||
"nextcloud" -> nextcloudService?.editDocument(documentId, content) ?: false
|
||||
"forgejo" -> forgejoService?.editDocument(documentId, content) ?: false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Share document from a specific service
|
||||
*/
|
||||
suspend fun shareDocument(service: String, documentId: String, permissions: String = "read"): String? {
|
||||
return when (service.lowercase()) {
|
||||
"onlyoffice" -> onlyOfficeService?.shareDocument(documentId, permissions)
|
||||
"nextcloud" -> nextcloudService?.shareDocument(documentId, permissions)
|
||||
"forgejo" -> forgejoService?.shareDocument(documentId, permissions)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List documents from a specific service
|
||||
*/
|
||||
suspend fun listDocuments(service: String): List<DocumentInfo> {
|
||||
return when (service.lowercase()) {
|
||||
"onlyoffice" -> onlyOfficeService?.listDocuments() ?: emptyList()
|
||||
"nextcloud" -> nextcloudService?.listDocuments() ?: emptyList()
|
||||
"forgejo" -> forgejoService?.listDocuments() ?: emptyList()
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get document info from a specific service
|
||||
*/
|
||||
suspend fun getDocumentInfo(service: String, documentId: String): DocumentInfo? {
|
||||
return when (service.lowercase()) {
|
||||
"onlyoffice" -> onlyOfficeService?.getDocumentInfo(documentId)
|
||||
"nextcloud" -> nextcloudService?.getDocumentInfo(documentId)
|
||||
"forgejo" -> forgejoService?.getDocumentInfo(documentId)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
suspend fun getConfig(): DocumentCollaborationConfig {
|
||||
val currentConfig = configurationManager.getCurrentConfig()
|
||||
return DocumentCollaborationConfig(
|
||||
onlyOffice = currentConfig.getIntegratedServiceConfig("onlyoffice") as? OnlyOfficeConfig ?: config.onlyOffice,
|
||||
nextcloud = currentConfig.getIntegratedServiceConfig("nextcloud") as? NextcloudConfig ?: config.nextcloud,
|
||||
forgejo = currentConfig.getIntegratedServiceConfig("forgejo") as? ForgejoConfig ?: config.forgejo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to get integrated service configuration from agent configuration
|
||||
*/
|
||||
private suspend fun AgentConfiguration.getIntegratedServiceConfig(service: String): Any? {
|
||||
return when (service) {
|
||||
"onlyoffice" -> {
|
||||
val aiConfig = aiProviders
|
||||
// Map to OnlyOffice config if available
|
||||
OnlyOfficeConfig(
|
||||
enabled = false, // Would come from custom config
|
||||
baseUrl = "", // Would come from custom config
|
||||
apiKey = "", // Would come from custom config
|
||||
secretKey = "" // Would come from custom config
|
||||
)
|
||||
}
|
||||
"nextcloud" -> {
|
||||
NextcloudConfig(
|
||||
enabled = false, // Would come from custom config
|
||||
baseUrl = "", // Would come from custom config
|
||||
username = "", // Would come from custom config
|
||||
password = "", // Would come from custom config
|
||||
appId = "", // Would come from custom config
|
||||
appPassword = "" // Would come from custom config
|
||||
)
|
||||
}
|
||||
"forgejo" -> {
|
||||
ForgejoConfig(
|
||||
enabled = false, // Would come from custom config
|
||||
baseUrl = "", // Would come from custom config
|
||||
token = "", // Would come from custom config
|
||||
apiVersion = "v1"
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,645 @@
|
|||
// Mixture and Fallback Manager for Aurelio Model Router
|
||||
// Handles mixing responses from multiple providers and fallback strategies
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Strategy for handling multiple provider responses
|
||||
*/
|
||||
enum class MixtureStrategy {
|
||||
WEIGHTED_AVERAGE, // Weight responses by provider reliability
|
||||
VOTE_BASED, // Use majority voting for consistency
|
||||
CONCATENATION, // Concatenate responses from multiple providers
|
||||
BEST_OF_N, // Return the best response from N providers
|
||||
CONFIDENCE_BASED // Weight by model's confidence scores
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy for fallback behavior
|
||||
*/
|
||||
enum class FallbackStrategy {
|
||||
SEQUENTIAL, // Try providers in sequence until one succeeds
|
||||
PARALLEL, // Try multiple providers in parallel, use first success
|
||||
CIRCUIT_BREAKER, // Track provider health, avoid unhealthy providers
|
||||
HYBRID // Combination of sequential and circuit breaker
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a response from a single provider in a mixture
|
||||
*/
|
||||
data class ProviderResponse(
|
||||
val provider: ModelProvider,
|
||||
val result: ModelResult,
|
||||
val weight: Double = 1.0,
|
||||
val confidence: Double = 1.0
|
||||
)
|
||||
|
||||
/**
|
||||
* Result from mixture processing
|
||||
*/
|
||||
data class MixtureResult(
|
||||
val success: Boolean,
|
||||
val content: String,
|
||||
val combinedResponses: List<ProviderResponse>,
|
||||
val mixtureStrategy: MixtureStrategy,
|
||||
val totalLatencyMs: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* Health status for a provider
|
||||
*/
|
||||
data class ProviderHealth(
|
||||
val isHealthy: Boolean,
|
||||
val lastResponseTime: Long,
|
||||
val successRate: Double,
|
||||
val failureCount: Int,
|
||||
val lastChecked: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* Manages mixtures and fallbacks for the model router
|
||||
*/
|
||||
class MixtureFallbackManager(private val modelRouter: ModelRouter) {
|
||||
private val healthStatus = ConcurrentHashMap<String, ProviderHealth>()
|
||||
private val mutex = Mutex()
|
||||
private val config = getConfig()
|
||||
|
||||
/**
|
||||
* Execute a request with mixture strategy
|
||||
*/
|
||||
suspend fun executeMixtureRequest(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>,
|
||||
strategy: MixtureStrategy = MixtureStrategy.WEIGHTED_AVERAGE
|
||||
): MixtureResult {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
if (!config.enableMixtures) {
|
||||
// If mixtures are disabled, just use the first provider
|
||||
val firstProvider = providers.firstOrNull() ?: config.defaultProvider
|
||||
val result = modelRouter.routeChatRequest(messages, firstProvider)
|
||||
|
||||
return MixtureResult(
|
||||
success = result.success,
|
||||
content = result.content,
|
||||
combinedResponses = listOf(ProviderResponse(
|
||||
provider = result.provider,
|
||||
result = result,
|
||||
weight = 1.0,
|
||||
confidence = if (result.success) 1.0 else 0.0
|
||||
)),
|
||||
mixtureStrategy = strategy,
|
||||
totalLatencyMs = System.currentTimeMillis() - startTime
|
||||
)
|
||||
}
|
||||
|
||||
// Execute requests based on strategy
|
||||
return when (strategy) {
|
||||
MixtureStrategy.WEIGHTED_AVERAGE -> executeWeightedAverage(messages, providers)
|
||||
MixtureStrategy.VOTE_BASED -> executeVoteBased(messages, providers)
|
||||
MixtureStrategy.CONCATENATION -> executeConcatenation(messages, providers)
|
||||
MixtureStrategy.BEST_OF_N -> executeBestOfN(messages, providers)
|
||||
MixtureStrategy.CONFIDENCE_BASED -> executeConfidenceBased(messages, providers)
|
||||
}.copy(totalLatencyMs = System.currentTimeMillis() - startTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute request with fallback strategy
|
||||
*/
|
||||
suspend fun executeFallbackRequest(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>,
|
||||
strategy: FallbackStrategy = FallbackStrategy.SEQUENTIAL
|
||||
): ModelResult {
|
||||
return when (strategy) {
|
||||
FallbackStrategy.SEQUENTIAL -> executeSequentialFallback(messages, providers)
|
||||
FallbackStrategy.PARALLEL -> executeParallelFallback(messages, providers)
|
||||
FallbackStrategy.CIRCUIT_BREAKER -> executeCircuitBreakerFallback(messages, providers)
|
||||
FallbackStrategy.HYBRID -> executeHybridFallback(messages, providers)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute weighted average mixture
|
||||
*/
|
||||
private suspend fun executeWeightedAverage(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>
|
||||
): MixtureResult {
|
||||
val responses = mutableListOf<ProviderResponse>()
|
||||
val weights = config.mixtureWeights
|
||||
|
||||
for (providerName in providers) {
|
||||
val weight = weights[providerName] ?: 0.1 // Default weight
|
||||
if (weight <= 0) continue // Skip providers with zero weight
|
||||
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
responses.add(ProviderResponse(
|
||||
provider = result.provider,
|
||||
result = result,
|
||||
weight = weight,
|
||||
confidence = if (result.success) calculateResponseQuality(result.content) else 0.0
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
// Log error but continue with other providers
|
||||
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Combine responses based on weights
|
||||
val combinedContent = combineWeightedResponses(responses)
|
||||
|
||||
return MixtureResult(
|
||||
success = responses.any { it.result.success },
|
||||
content = combinedContent,
|
||||
combinedResponses = responses,
|
||||
mixtureStrategy = MixtureStrategy.WEIGHTED_AVERAGE,
|
||||
totalLatencyMs = 0 // Set by caller
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute vote-based mixture
|
||||
*/
|
||||
private suspend fun executeVoteBased(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>
|
||||
): MixtureResult {
|
||||
val responses = mutableListOf<ProviderResponse>()
|
||||
|
||||
for (providerName in providers) {
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
responses.add(ProviderResponse(
|
||||
provider = result.provider,
|
||||
result = result,
|
||||
weight = 1.0,
|
||||
confidence = if (result.success) calculateResponseQuality(result.content) else 0.0
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Find the most common response or best quality response
|
||||
val bestResponse = responses.filter { it.result.success }
|
||||
.maxByOrNull { it.confidence } ?: responses.firstOrNull()
|
||||
|
||||
return MixtureResult(
|
||||
success = bestResponse?.result?.success == true,
|
||||
content = bestResponse?.result?.content ?: "",
|
||||
combinedResponses = responses,
|
||||
mixtureStrategy = MixtureStrategy.VOTE_BASED,
|
||||
totalLatencyMs = 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute concatenation mixture
|
||||
*/
|
||||
private suspend fun executeConcatenation(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>
|
||||
): MixtureResult {
|
||||
val responses = mutableListOf<ProviderResponse>()
|
||||
val successfulContents = mutableListOf<String>()
|
||||
|
||||
for (providerName in providers) {
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
responses.add(ProviderResponse(
|
||||
provider = result.provider,
|
||||
result = result,
|
||||
weight = 1.0,
|
||||
confidence = if (result.success) calculateResponseQuality(result.content) else 0.0
|
||||
))
|
||||
|
||||
if (result.success) {
|
||||
successfulContents.add("${result.provider.name.toUpperCase()}: ${result.content}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
val concatenatedContent = successfulContents.joinToString("\n\n---\n\n")
|
||||
|
||||
return MixtureResult(
|
||||
success = successfulContents.isNotEmpty(),
|
||||
content = concatenatedContent,
|
||||
combinedResponses = responses,
|
||||
mixtureStrategy = MixtureStrategy.CONCATENATION,
|
||||
totalLatencyMs = 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute best-of-N mixture
|
||||
*/
|
||||
private suspend fun executeBestOfN(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>
|
||||
): MixtureResult {
|
||||
val responses = mutableListOf<ProviderResponse>()
|
||||
|
||||
for (providerName in providers) {
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
responses.add(ProviderResponse(
|
||||
provider = result.provider,
|
||||
result = result,
|
||||
weight = 1.0,
|
||||
confidence = if (result.success) calculateResponseQuality(result.content) else 0.0
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Return the response with highest confidence
|
||||
val bestResponse = responses.filter { it.result.success }
|
||||
.maxByOrNull { it.confidence }
|
||||
|
||||
return MixtureResult(
|
||||
success = bestResponse?.result?.success == true,
|
||||
content = bestResponse?.result?.content ?: "",
|
||||
combinedResponses = responses,
|
||||
mixtureStrategy = MixtureStrategy.BEST_OF_N,
|
||||
totalLatencyMs = 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute confidence-based mixture
|
||||
*/
|
||||
private suspend fun executeConfidenceBased(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>
|
||||
): MixtureResult {
|
||||
val responses = mutableListOf<ProviderResponse>()
|
||||
|
||||
for (providerName in providers) {
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
val confidence = if (result.success) {
|
||||
calculateResponseConfidence(result, providerName)
|
||||
} else 0.0
|
||||
|
||||
responses.add(ProviderResponse(
|
||||
provider = result.provider,
|
||||
result = result,
|
||||
weight = confidence,
|
||||
confidence = confidence
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Combine based on confidence scores
|
||||
val combinedContent = combineConfidenceBasedResponses(responses)
|
||||
|
||||
return MixtureResult(
|
||||
success = responses.any { it.result.success },
|
||||
content = combinedContent,
|
||||
combinedResponses = responses,
|
||||
mixtureStrategy = MixtureStrategy.CONFIDENCE_BASED,
|
||||
totalLatencyMs = 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute sequential fallback
|
||||
*/
|
||||
private suspend fun executeSequentialFallback(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>
|
||||
): ModelResult {
|
||||
for (providerName in providers) {
|
||||
// Check health if using circuit breaker approach
|
||||
if (isProviderHealthy(providerName)) {
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
|
||||
// Update health status
|
||||
updateProviderHealth(providerName, result.latencyMs, result.success)
|
||||
|
||||
if (result.success) {
|
||||
return result
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
updateProviderHealth(providerName, 0, false)
|
||||
println("[MixtureFallbackManager] Sequential fallback failed for $providerName: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all providers failed, return the last error
|
||||
return ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = "none",
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = 0,
|
||||
error = "All providers failed in sequential fallback"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute parallel fallback
|
||||
*/
|
||||
private suspend fun executeParallelFallback(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>
|
||||
): ModelResult = withContext(Dispatchers.IO) {
|
||||
val deferredResults = providers.map { providerName ->
|
||||
async {
|
||||
if (isProviderHealthy(providerName)) {
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
updateProviderHealth(providerName, result.latencyMs, result.success)
|
||||
Pair(providerName, result)
|
||||
} catch (e: Exception) {
|
||||
updateProviderHealth(providerName, 0, false)
|
||||
Pair(providerName, ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = providerName,
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = 0,
|
||||
error = e.message ?: "Unknown error"
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Pair(providerName, ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = providerName,
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = 0,
|
||||
error = "Provider marked as unhealthy"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val results = awaitAll(*deferredResults.toTypedArray())
|
||||
|
||||
// Return the first successful result
|
||||
val successfulResult = results.find { it.second.success }
|
||||
return@withContext successfulResult?.second ?: results.firstOrNull()?.second ?: ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = "none",
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = 0,
|
||||
error = "All providers failed in parallel fallback"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute circuit breaker fallback
|
||||
*/
|
||||
private suspend fun executeCircuitBreakerFallback(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>
|
||||
): ModelResult {
|
||||
val healthyProviders = providers.filter { isProviderHealthy(it) }
|
||||
|
||||
for (providerName in healthyProviders) {
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
|
||||
// Update health status
|
||||
updateProviderHealth(providerName, result.latencyMs, result.success)
|
||||
|
||||
if (result.success) {
|
||||
return result
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
updateProviderHealth(providerName, 0, false)
|
||||
println("[MixtureFallbackManager] Circuit breaker fallback failed for $providerName: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// If no healthy providers worked, try unhealthy ones as last resort
|
||||
val unhealthyProviders = providers.filterNot { isProviderHealthy(it) }
|
||||
for (providerName in unhealthyProviders) {
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
|
||||
// Update health status
|
||||
updateProviderHealth(providerName, result.latencyMs, result.success)
|
||||
|
||||
if (result.success) {
|
||||
return result
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
updateProviderHealth(providerName, 0, false)
|
||||
}
|
||||
}
|
||||
|
||||
return ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = "none",
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = 0,
|
||||
error = "All providers failed in circuit breaker fallback"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute hybrid fallback combining sequential and circuit breaker
|
||||
*/
|
||||
private suspend fun executeHybridFallback(
|
||||
messages: List<ChatMessage>,
|
||||
providers: List<String>
|
||||
): ModelResult {
|
||||
// First try healthy providers sequentially
|
||||
val healthyProviders = providers.filter { isProviderHealthy(it) }
|
||||
|
||||
for (providerName in healthyProviders) {
|
||||
try {
|
||||
val result = modelRouter.routeChatRequest(messages, providerName)
|
||||
|
||||
// Update health status
|
||||
updateProviderHealth(providerName, result.latencyMs, result.success)
|
||||
|
||||
if (result.success) {
|
||||
return result
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
updateProviderHealth(providerName, 0, false)
|
||||
println("[MixtureFallbackManager] Hybrid fallback failed for healthy $providerName: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Then try remaining providers in parallel as a last resort
|
||||
val remainingProviders = providers.filterNot { it in healthyProviders }
|
||||
if (remainingProviders.isNotEmpty()) {
|
||||
return executeParallelFallback(messages, remainingProviders)
|
||||
}
|
||||
|
||||
return ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = "none",
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = 0,
|
||||
error = "All providers failed in hybrid fallback"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a quality score for a response
|
||||
*/
|
||||
private fun calculateResponseQuality(content: String): Double {
|
||||
// Simple heuristic for response quality
|
||||
if (content.isBlank()) return 0.0
|
||||
|
||||
var score = 0.5 // Base score
|
||||
|
||||
// Reward longer, more substantial responses
|
||||
if (content.length > 100) score += 0.2
|
||||
if (content.length > 500) score += 0.1
|
||||
|
||||
// Reward variety in content
|
||||
val uniqueWords = content.lowercase()
|
||||
.split("\\W+".toRegex())
|
||||
.filter { it.length > 3 }
|
||||
.distinct()
|
||||
.size
|
||||
if (uniqueWords > 20) score += 0.1
|
||||
if (uniqueWords > 50) score += 0.1
|
||||
|
||||
// Cap at 1.0
|
||||
return min(score, 1.0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate confidence based on provider and response
|
||||
*/
|
||||
private fun calculateResponseConfidence(result: ModelResult, providerName: String): Double {
|
||||
var confidence = if (result.success) 0.8 else 0.0
|
||||
|
||||
// Adjust based on provider reliability
|
||||
val health = healthStatus[providerName]
|
||||
if (health != null) {
|
||||
confidence *= health.successRate
|
||||
}
|
||||
|
||||
// Adjust based on response quality
|
||||
if (result.success) {
|
||||
confidence *= calculateResponseQuality(result.content)
|
||||
}
|
||||
|
||||
return min(confidence, 1.0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine responses using weighted averaging
|
||||
*/
|
||||
private fun combineWeightedResponses(responses: List<ProviderResponse>): String {
|
||||
if (responses.isEmpty()) return ""
|
||||
if (responses.size == 1) return responses.first().result.content
|
||||
|
||||
// For now, return the response with the highest weight * confidence
|
||||
val bestResponse = responses.maxByOrNull { it.weight * it.confidence }
|
||||
|
||||
return bestResponse?.result?.content ?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine responses using confidence-based weighting
|
||||
*/
|
||||
private fun combineConfidenceBasedResponses(responses: List<ProviderResponse>): String {
|
||||
if (responses.isEmpty()) return ""
|
||||
if (responses.size == 1) return responses.first().result.content
|
||||
|
||||
// Return the highest confidence response
|
||||
val bestResponse = responses.maxByOrNull { it.confidence }
|
||||
|
||||
return bestResponse?.result?.content ?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is considered healthy
|
||||
*/
|
||||
private fun isProviderHealthy(providerName: String): Boolean {
|
||||
val health = healthStatus[providerName] ?: return true // Assume healthy if not tracked
|
||||
|
||||
// Unhealthy if failure count is high or success rate is low
|
||||
return health.successRate > 0.3 && health.failureCount < 5
|
||||
}
|
||||
|
||||
/**
|
||||
* Update provider health status
|
||||
*/
|
||||
private suspend fun updateProviderHealth(providerName: String, responseTime: Long, success: Boolean) {
|
||||
mutex.withLock {
|
||||
val currentHealth = healthStatus[providerName]
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val newFailureCount = if (success) 0 else ((currentHealth?.failureCount ?: 0) + 1)
|
||||
val newSuccessCount = if (success) ((currentHealth?.successRate?.times(if (currentHealth != null) 10 else 0) ?: 0) + 1) else (currentHealth?.successRate?.times(if (currentHealth != null) 10 else 0) ?: 0)
|
||||
|
||||
// Calculate new success rate (simple average of last 10 attempts)
|
||||
val totalAttempts = min(newSuccessCount + newFailureCount, 10.0)
|
||||
val newSuccessRate = if (totalAttempts > 0) newSuccessCount / totalAttempts else 1.0
|
||||
|
||||
val newHealth = ProviderHealth(
|
||||
isHealthy = newSuccessRate > 0.3 && newFailureCount < 5,
|
||||
lastResponseTime = responseTime,
|
||||
successRate = newSuccessRate,
|
||||
failureCount = newFailureCount,
|
||||
lastChecked = now
|
||||
)
|
||||
|
||||
healthStatus[providerName] = newHealth
|
||||
|
||||
// Log health changes
|
||||
if (currentHealth?.isHealthy != newHealth.isHealthy) {
|
||||
val statusChange = if (newHealth.isHealthy) "HEALTHY" else "UNHEALTHY"
|
||||
println("[MixtureFallbackManager] Provider $providerName marked as $statusChange (success rate: ${String.format("%.2f", newSuccessRate)})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health status for all providers
|
||||
*/
|
||||
fun getAllHealthStatus(): Map<String, ProviderHealth> {
|
||||
return healthStatus.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset health status for a provider
|
||||
*/
|
||||
suspend fun resetProviderHealth(providerName: String) {
|
||||
mutex.withLock {
|
||||
healthStatus.remove(providerName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodically refresh health status (should be called regularly)
|
||||
*/
|
||||
suspend fun refreshHealthStatus() {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
mutex.withLock {
|
||||
// Remove stale health records (older than 10 minutes)
|
||||
healthStatus.entries.removeIf { entry ->
|
||||
now - entry.value.lastChecked > 600000 // 10 minutes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
674
extensions/aurelio-jetbrains/src/main/kotlin/ModelRouter.kt
Normal file
674
extensions/aurelio-jetbrains/src/main/kotlin/ModelRouter.kt
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
// Model Router for Aurelio - Inspired by Hermes Agent capabilities
|
||||
// Supports multiple AI providers with fallbacks and local LLM integration
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.*
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* Represents different AI model providers supported by Aurelio
|
||||
*/
|
||||
enum class ModelProvider {
|
||||
HERMES_NVIDIA_NIM, // Primary: Hermes Agent with NVIDIA NIM
|
||||
LOCAL_OLLAMA, // Fallback: Local Ollama
|
||||
OPENAI, // Cloud: OpenAI GPT models
|
||||
ANTHROPIC, // Cloud: Anthropic Claude models
|
||||
GOOGLE_VERTEX, // Cloud: Google Vertex AI
|
||||
MISTRAL, // Cloud: Mistral AI
|
||||
TOGETHER, // Cloud: Together AI
|
||||
CUSTOM_HTTP, // Custom HTTP endpoint
|
||||
LOCAL_LM_STUDIO // Local LM Studio compatible
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a specific model provider
|
||||
*/
|
||||
data class ProviderConfig(
|
||||
val provider: ModelProvider,
|
||||
val modelName: String,
|
||||
val apiKey: String? = null,
|
||||
val baseUrl: String? = null,
|
||||
val fallbackProviders: List<ModelProvider> = emptyList(),
|
||||
val temperature: Double = 0.7,
|
||||
val maxTokens: Int = 2048,
|
||||
val timeoutMs: Long = 30000
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents a chat message in the conversation
|
||||
*/
|
||||
data class ChatMessage(
|
||||
val role: String, // "user", "assistant", "system"
|
||||
val content: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Result from a model provider
|
||||
*/
|
||||
data class ModelResult(
|
||||
val success: Boolean,
|
||||
val content: String,
|
||||
val modelUsed: String,
|
||||
val provider: ModelProvider,
|
||||
val latencyMs: Long,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Main model router that handles requests and provider fallbacks
|
||||
*/
|
||||
class ModelRouter {
|
||||
private val objectMapper: ObjectMapper = jacksonObjectMapper()
|
||||
private val httpClient = OkHttpClient.Builder()
|
||||
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
// Provider configs
|
||||
private val providerConfigs = mutableMapOf<String, ProviderConfig>()
|
||||
|
||||
// Stats tracking
|
||||
private val requestCounter = AtomicInteger(0)
|
||||
private val successCounter = AtomicInteger(0)
|
||||
|
||||
init {
|
||||
// Initialize with default configurations
|
||||
setupDefaultProviders()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up default provider configurations based on hermes-agent capabilities
|
||||
*/
|
||||
private fun setupDefaultProviders() {
|
||||
// Primary: Hermes Agent with NVIDIA NIM (as used in hermes-agent)
|
||||
providerConfigs["hermes-nvidia"] = ProviderConfig(
|
||||
provider = ModelProvider.HERMES_NVIDIA_NIM,
|
||||
modelName = "nvidia/nemotron-3-ultra-550b-a55b",
|
||||
baseUrl = System.getenv("NVIDIA_NIM_API_BASE") ?: "https://integrate.api.nvidia.com/v1",
|
||||
apiKey = System.getenv("NVIDIA_NIM_API_KEY"),
|
||||
fallbackProviders = listOf(ModelProvider.LOCAL_OLLAMA),
|
||||
temperature = 0.7,
|
||||
maxTokens = 4096
|
||||
)
|
||||
|
||||
// Fallback: Local Ollama (as used in hermes-agent)
|
||||
providerConfigs["local-ollama"] = ProviderConfig(
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
modelName = System.getenv("OLLAMA_MODEL") ?: "llama3.2",
|
||||
baseUrl = System.getenv("OLLAMA_BASE_URL") ?: "http://localhost:11434",
|
||||
fallbackProviders = listOf(ModelProvider.OPENAI),
|
||||
temperature = 0.7,
|
||||
maxTokens = 2048
|
||||
)
|
||||
|
||||
// Cloud providers
|
||||
providerConfigs["openai"] = ProviderConfig(
|
||||
provider = ModelProvider.OPENAI,
|
||||
modelName = System.getenv("OPENAI_MODEL") ?: "gpt-4o",
|
||||
baseUrl = System.getenv("OPENAI_BASE_URL") ?: "https://api.openai.com/v1",
|
||||
apiKey = System.getenv("OPENAI_API_KEY"),
|
||||
fallbackProviders = listOf(ModelProvider.ANTHROPIC, ModelProvider.GOOGLE_VERTEX),
|
||||
temperature = 0.7,
|
||||
maxTokens = 4096
|
||||
)
|
||||
|
||||
providerConfigs["anthropic"] = ProviderConfig(
|
||||
provider = ModelProvider.ANTHROPIC,
|
||||
modelName = System.getenv("ANTHROPIC_MODEL") ?: "claude-3-5-sonnet-20241022",
|
||||
baseUrl = System.getenv("ANTHROPIC_BASE_URL") ?: "https://api.anthropic.com/v1",
|
||||
apiKey = System.getenv("ANTHROPIC_API_KEY"),
|
||||
fallbackProviders = listOf(ModelProvider.GOOGLE_VERTEX, ModelProvider.MISTRAL),
|
||||
temperature = 0.7,
|
||||
maxTokens = 4096
|
||||
)
|
||||
|
||||
providerConfigs["google-vertex"] = ProviderConfig(
|
||||
provider = ModelProvider.GOOGLE_VERTEX,
|
||||
modelName = System.getenv("GOOGLE_VERTEX_MODEL") ?: "gemini-2.0-flash-exp",
|
||||
baseUrl = System.getenv("GOOGLE_VERTEX_BASE_URL") ?: "https://generativelanguage.googleapis.com/v1beta",
|
||||
apiKey = System.getenv("GOOGLE_VERTEX_API_KEY"),
|
||||
fallbackProviders = listOf(ModelProvider.MISTRAL, ModelProvider.TOGETHER),
|
||||
temperature = 0.7,
|
||||
maxTokens = 2048
|
||||
)
|
||||
|
||||
providerConfigs["mistral"] = ProviderConfig(
|
||||
provider = ModelProvider.MISTRAL,
|
||||
modelName = System.getenv("MISTRAL_MODEL") ?: "mistral-large-latest",
|
||||
baseUrl = System.getenv("MISTRAL_BASE_URL") ?: "https://api.mistral.ai/v1",
|
||||
apiKey = System.getenv("MISTRAL_API_KEY"),
|
||||
fallbackProviders = listOf(ModelProvider.TOGETHER, ModelProvider.OPENAI),
|
||||
temperature = 0.7,
|
||||
maxTokens = 2048
|
||||
)
|
||||
|
||||
providerConfigs["together"] = ProviderConfig(
|
||||
provider = ModelProvider.TOGETHER,
|
||||
modelName = System.getenv("TOGETHER_MODEL") ?: "meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo",
|
||||
baseUrl = System.getenv("TOGETHER_BASE_URL") ?: "https://api.together.xyz/v1",
|
||||
apiKey = System.getenv("TOGETHER_API_KEY"),
|
||||
fallbackProviders = listOf(ModelProvider.LOCAL_OLLAMA),
|
||||
temperature = 0.7,
|
||||
maxTokens = 2048
|
||||
)
|
||||
|
||||
// Local LM Studio compatible
|
||||
providerConfigs["local-lmstudio"] = ProviderConfig(
|
||||
provider = ModelProvider.LOCAL_LM_STUDIO,
|
||||
modelName = System.getenv("LMSTUDIO_MODEL") ?: "local-model",
|
||||
baseUrl = System.getenv("LMSTUDIO_BASE_URL") ?: "http://localhost:1234/v1",
|
||||
fallbackProviders = listOf(ModelProvider.LOCAL_OLLAMA),
|
||||
temperature = 0.7,
|
||||
maxTokens = 2048
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Route a chat request through available providers with fallback logic
|
||||
*/
|
||||
suspend fun routeChatRequest(
|
||||
messages: List<ChatMessage>,
|
||||
selectedProvider: String? = null,
|
||||
customConfig: ProviderConfig? = null
|
||||
): ModelResult = withContext(Dispatchers.IO) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
val requestId = requestCounter.incrementAndGet()
|
||||
|
||||
println("[ModelRouter] Processing request #${requestId} with ${messages.size} messages")
|
||||
|
||||
// Determine which providers to try
|
||||
val providersToTry = if (selectedProvider != null) {
|
||||
// If a specific provider is selected, try it and its fallbacks
|
||||
val primaryConfig = customConfig ?: providerConfigs[selectedProvider]
|
||||
if (primaryConfig != null) {
|
||||
listOf(primaryConfig.provider.name.lowercase()) +
|
||||
primaryConfig.fallbackProviders.map { it.name.lowercase() }
|
||||
} else {
|
||||
// If the selected provider doesn't exist, use defaults
|
||||
getDefaultProviderOrder()
|
||||
}
|
||||
} else {
|
||||
// Use default provider order
|
||||
getDefaultProviderOrder()
|
||||
}
|
||||
|
||||
var lastError: Exception? = null
|
||||
|
||||
// Try each provider in order
|
||||
for (providerName in providersToTry) {
|
||||
val config = providerConfigs[providerName]
|
||||
if (config == null) {
|
||||
println("[ModelRouter] Warning: Provider configuration not found for $providerName")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
println("[ModelRouter] Attempting provider: ${config.provider} (${config.modelName})")
|
||||
|
||||
val result = when (config.provider) {
|
||||
ModelProvider.HERMES_NVIDIA_NIM -> callNvidiaNimApi(config, messages)
|
||||
ModelProvider.LOCAL_OLLAMA -> callOllamaApi(config, messages)
|
||||
ModelProvider.OPENAI -> callOpenAiApi(config, messages)
|
||||
ModelProvider.ANTHROPIC -> callAnthropicApi(config, messages)
|
||||
ModelProvider.GOOGLE_VERTEX -> callGoogleVertexApi(config, messages)
|
||||
ModelProvider.MISTRAL -> callMistralApi(config, messages)
|
||||
ModelProvider.TOGETHER -> callTogetherApi(config, messages)
|
||||
ModelProvider.LOCAL_LM_STUDIO -> callLmStudioApi(config, messages)
|
||||
ModelProvider.CUSTOM_HTTP -> callCustomHttpApi(config, messages)
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
val latency = System.currentTimeMillis() - startTime
|
||||
println("[ModelRouter] Success with ${config.provider} after ${latency}ms (#$requestId)")
|
||||
successCounter.incrementAndGet()
|
||||
return@withContext result.copy(latencyMs = latency)
|
||||
} else {
|
||||
println("[ModelRouter] Provider ${config.provider} failed: ${result.error}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[ModelRouter] Provider ${config.provider} threw exception: ${e.message}")
|
||||
lastError = e
|
||||
}
|
||||
}
|
||||
|
||||
// All providers failed
|
||||
val latency = System.currentTimeMillis() - startTime
|
||||
val errorMsg = lastError?.message ?: "All providers failed"
|
||||
println("[ModelRouter] All providers failed after ${latency}ms (#$requestId): $errorMsg")
|
||||
|
||||
ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = "none",
|
||||
provider = ModelProvider.LOCAL_OLLAMA, // Default fallback
|
||||
latencyMs = latency,
|
||||
error = errorMsg
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default provider order based on hermes-agent architecture
|
||||
*/
|
||||
private fun getDefaultProviderOrder(): List<String> {
|
||||
return listOf(
|
||||
"hermes-nvidia", // Primary: NVIDIA NIM (Hermes Agent)
|
||||
"local-ollama", // Fallback: Local Ollama (Hermes Agent fallback)
|
||||
"openai", // Cloud option
|
||||
"anthropic", // Cloud option
|
||||
"google-vertex", // Cloud option
|
||||
"mistral", // Cloud option
|
||||
"together", // Cloud option
|
||||
"local-lmstudio" // Local option
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Call NVIDIA NIM API (used by Hermes Agent)
|
||||
*/
|
||||
private suspend fun callNvidiaNimApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
|
||||
return callOpenAiCompatibleApi(config, messages, "nvidia-nim")
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Ollama API (used by Hermes Agent as fallback)
|
||||
*/
|
||||
private suspend fun callOllamaApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
|
||||
val requestUrl = "${config.baseUrl}/api/chat"
|
||||
|
||||
val requestBody = mapOf(
|
||||
"model" to config.modelName,
|
||||
"messages" to messages,
|
||||
"stream" to false,
|
||||
"options" to mapOf(
|
||||
"temperature" to config.temperature,
|
||||
"num_predict" to config.maxTokens
|
||||
)
|
||||
)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(requestUrl)
|
||||
.post(okhttp3.RequestBody.create(
|
||||
MediaType.get("application/json"),
|
||||
objectMapper.writeValueAsString(requestBody)
|
||||
))
|
||||
.apply {
|
||||
if (!config.apiKey.isNullOrEmpty()) {
|
||||
addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
return try {
|
||||
val response = httpClient.newCall(request).await()
|
||||
val responseBody = response.body?.string()
|
||||
|
||||
if (response.isSuccessful && responseBody != null) {
|
||||
val parsedResponse = objectMapper.readTree(responseBody)
|
||||
val content = parsedResponse.at("/message/content").asText()
|
||||
|
||||
ModelResult(
|
||||
success = true,
|
||||
content = content,
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = 0 // Will be calculated by caller
|
||||
)
|
||||
} else {
|
||||
ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = 0,
|
||||
error = "HTTP ${response.code}: ${responseBody}"
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.LOCAL_OLLAMA,
|
||||
latencyMs = 0,
|
||||
error = e.message ?: "Unknown error"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call OpenAI-compatible API
|
||||
*/
|
||||
private suspend fun callOpenAiCompatibleApi(config: ProviderConfig, messages: List<ChatMessage>, providerName: String): ModelResult {
|
||||
val requestUrl = "${config.baseUrl}/chat/completions"
|
||||
|
||||
val requestBody = mapOf(
|
||||
"model" to config.modelName,
|
||||
"messages" to messages,
|
||||
"temperature" to config.temperature,
|
||||
"max_tokens" to config.maxTokens,
|
||||
"stream" to false
|
||||
)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(requestUrl)
|
||||
.post(okhttp3.RequestBody.create(
|
||||
MediaType.get("application/json"),
|
||||
objectMapper.writeValueAsString(requestBody)
|
||||
))
|
||||
.header("Content-Type", "application/json")
|
||||
.apply {
|
||||
if (!config.apiKey.isNullOrEmpty()) {
|
||||
when (providerName) {
|
||||
"nvidia-nim" -> addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
"openai" -> addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
"mistral" -> addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
"together" -> addHeader("Authorization", "Bearer ${config.apiKey}")
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
return try {
|
||||
val response = httpClient.newCall(request).await()
|
||||
val responseBody = response.body?.string()
|
||||
|
||||
if (response.isSuccessful && responseBody != null) {
|
||||
val parsedResponse = objectMapper.readTree(responseBody)
|
||||
val content = parsedResponse.at("/choices/0/message/content").asText()
|
||||
|
||||
ModelResult(
|
||||
success = true,
|
||||
content = content,
|
||||
modelUsed = config.modelName,
|
||||
provider = when (providerName) {
|
||||
"nvidia-nim" -> ModelProvider.HERMES_NVIDIA_NIM
|
||||
"openai" -> ModelProvider.OPENAI
|
||||
"mistral" -> ModelProvider.MISTRAL
|
||||
"together" -> ModelProvider.TOGETHER
|
||||
else -> ModelProvider.CUSTOM_HTTP
|
||||
},
|
||||
latencyMs = 0 // Will be calculated by caller
|
||||
)
|
||||
} else {
|
||||
ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = config.modelName,
|
||||
provider = when (providerName) {
|
||||
"nvidia-nim" -> ModelProvider.HERMES_NVIDIA_NIM
|
||||
"openai" -> ModelProvider.OPENAI
|
||||
"mistral" -> ModelProvider.MISTRAL
|
||||
"together" -> ModelProvider.TOGETHER
|
||||
else -> ModelProvider.CUSTOM_HTTP
|
||||
},
|
||||
latencyMs = 0,
|
||||
error = "HTTP ${response.code}: ${responseBody}"
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = config.modelName,
|
||||
provider = when (providerName) {
|
||||
"nvidia-nim" -> ModelProvider.HERMES_NVIDIA_NIM
|
||||
"openai" -> ModelProvider.OPENAI
|
||||
"mistral" -> ModelProvider.MISTRAL
|
||||
"together" -> ModelProvider.TOGETHER
|
||||
else -> ModelProvider.CUSTOM_HTTP
|
||||
},
|
||||
latencyMs = 0,
|
||||
error = e.message ?: "Unknown error"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call OpenAI API specifically
|
||||
*/
|
||||
private suspend fun callOpenAiApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
|
||||
return callOpenAiCompatibleApi(config, messages, "openai")
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Anthropic API
|
||||
*/
|
||||
private suspend fun callAnthropicApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
|
||||
val requestUrl = "${config.baseUrl}/messages"
|
||||
|
||||
// Convert messages to Anthropic format
|
||||
val anthropicMessages = messages.filter { it.role != "system" }.map { msg ->
|
||||
mapOf("role" to if (msg.role == "user") "user" else "assistant", "content" to msg.content)
|
||||
}
|
||||
|
||||
val systemMessage = messages.firstOrNull { it.role == "system" }?.content
|
||||
|
||||
val requestBody = mutableMapOf<String, Any>(
|
||||
"model" to config.modelName,
|
||||
"messages" to anthropicMessages,
|
||||
"max_tokens" to config.maxTokens,
|
||||
"temperature" to config.temperature
|
||||
)
|
||||
|
||||
if (!systemMessage.isNullOrEmpty()) {
|
||||
requestBody["system"] = systemMessage
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(requestUrl)
|
||||
.post(okhttp3.RequestBody.create(
|
||||
MediaType.get("application/json"),
|
||||
objectMapper.writeValueAsString(requestBody)
|
||||
))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-API-Key", config.apiKey ?: "")
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.build()
|
||||
|
||||
return try {
|
||||
val response = httpClient.newCall(request).await()
|
||||
val responseBody = response.body?.string()
|
||||
|
||||
if (response.isSuccessful && responseBody != null) {
|
||||
val parsedResponse = objectMapper.readTree(responseBody)
|
||||
val content = parsedResponse.at("/content/0/text").asText()
|
||||
|
||||
ModelResult(
|
||||
success = true,
|
||||
content = content,
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.ANTHROPIC,
|
||||
latencyMs = 0 // Will be calculated by caller
|
||||
)
|
||||
} else {
|
||||
ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.ANTHROPIC,
|
||||
latencyMs = 0,
|
||||
error = "HTTP ${response.code}: ${responseBody}"
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.ANTHROPIC,
|
||||
latencyMs = 0,
|
||||
error = e.message ?: "Unknown error"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Google Vertex AI API
|
||||
*/
|
||||
private suspend fun callGoogleVertexApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
|
||||
val modelName = config.modelName.removePrefix("gemini-")
|
||||
val requestUrl = "${config.baseUrl}/models/$modelName:generateContent"
|
||||
|
||||
// Convert messages to Vertex format
|
||||
val contents = messages.map { msg ->
|
||||
mapOf(
|
||||
"role" to if (msg.role == "user" || msg.role == "system") "user" else "model",
|
||||
"parts" to listOf(mapOf("text" to msg.content))
|
||||
)
|
||||
}
|
||||
|
||||
val requestBody = mapOf(
|
||||
"contents" to contents,
|
||||
"generationConfig" to mapOf(
|
||||
"temperature" to config.temperature,
|
||||
"maxOutputTokens" to config.maxTokens
|
||||
)
|
||||
)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(requestUrl)
|
||||
.post(okhttp3.RequestBody.create(
|
||||
MediaType.get("application/json"),
|
||||
objectMapper.writeValueAsString(requestBody)
|
||||
))
|
||||
.header("Content-Type", "application/json")
|
||||
.apply {
|
||||
if (!config.apiKey.isNullOrEmpty()) {
|
||||
addHeader("x-goog-api-key", config.apiKey)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
return try {
|
||||
val response = httpClient.newCall(request).await()
|
||||
val responseBody = response.body?.string()
|
||||
|
||||
if (response.isSuccessful && responseBody != null) {
|
||||
val parsedResponse = objectMapper.readTree(responseBody)
|
||||
val content = parsedResponse.at("/candidates/0/content/parts/0/text").asText()
|
||||
|
||||
ModelResult(
|
||||
success = true,
|
||||
content = content,
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.GOOGLE_VERTEX,
|
||||
latencyMs = 0 // Will be calculated by caller
|
||||
)
|
||||
} else {
|
||||
ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.GOOGLE_VERTEX,
|
||||
latencyMs = 0,
|
||||
error = "HTTP ${response.code}: ${responseBody}"
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.GOOGLE_VERTEX,
|
||||
latencyMs = 0,
|
||||
error = e.message ?: "Unknown error"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Mistral API
|
||||
*/
|
||||
private suspend fun callMistralApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
|
||||
return callOpenAiCompatibleApi(config, messages, "mistral")
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Together API
|
||||
*/
|
||||
private suspend fun callTogetherApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
|
||||
return callOpenAiCompatibleApi(config, messages, "together")
|
||||
}
|
||||
|
||||
/**
|
||||
* Call LM Studio compatible API
|
||||
*/
|
||||
private suspend fun callLmStudioApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
|
||||
return callOpenAiCompatibleApi(config, messages, "lmstudio")
|
||||
}
|
||||
|
||||
/**
|
||||
* Call custom HTTP API
|
||||
*/
|
||||
private suspend fun callCustomHttpApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
|
||||
// This would be customized based on the specific endpoint
|
||||
return ModelResult(
|
||||
success = false,
|
||||
content = "",
|
||||
modelUsed = config.modelName,
|
||||
provider = ModelProvider.CUSTOM_HTTP,
|
||||
latencyMs = 0,
|
||||
error = "Custom HTTP provider not implemented"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current statistics
|
||||
*/
|
||||
fun getStats(): Map<String, Any> {
|
||||
return mapOf(
|
||||
"totalRequests" to requestCounter.get(),
|
||||
"successfulRequests" to successCounter.get(),
|
||||
"successRate" to if (requestCounter.get() > 0) {
|
||||
successCounter.get().toDouble() / requestCounter.get()
|
||||
} else 0.0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a provider configuration
|
||||
*/
|
||||
fun addProviderConfig(name: String, config: ProviderConfig) {
|
||||
providerConfigs[name] = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available providers
|
||||
*/
|
||||
fun getAvailableProviders(): List<String> {
|
||||
return providerConfigs.keys.toList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to make OkHttp calls suspending
|
||||
*/
|
||||
suspend fun Call.await(): okhttp3.Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
this.cancel()
|
||||
}
|
||||
|
||||
this.enqueue(object : okhttp3.Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (!continuation.isCompleted) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: okhttp3.Response) {
|
||||
if (!continuation.isCompleted) {
|
||||
continuation.resume(response) { _, _ ->
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
// Platform Integration for Document Collaboration Services
|
||||
// Provides integration points for Aurelio-Theia, Aurelio-VSCode, and Aurelio-Web
|
||||
|
||||
package org.intellij.sdk.language
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Interface for platform-specific document collaboration integration
|
||||
*/
|
||||
interface PlatformDocumentCollaborationIntegration {
|
||||
fun initialize()
|
||||
fun registerCommands()
|
||||
fun connectToServices()
|
||||
fun disconnectFromServices()
|
||||
fun uploadFile(filePath: String, serviceName: String): Boolean
|
||||
fun downloadFile(remotePath: String, localPath: String, serviceName: String): Boolean
|
||||
fun createDocument(title: String, content: String, serviceName: String): String?
|
||||
fun editDocument(documentId: String, content: String, serviceName: String): Boolean
|
||||
fun shareDocument(documentId: String, serviceName: String): String?
|
||||
fun listDocuments(serviceName: String): List<DocumentInfo>
|
||||
fun getDocumentInfo(documentId: String, serviceName: String): DocumentInfo?
|
||||
}
|
||||
|
||||
/**
|
||||
* Aurelio VSCode Integration
|
||||
*/
|
||||
class AurelioVSCodeIntegration(
|
||||
private val documentCollaborationManager: DocumentCollaborationManager
|
||||
) : PlatformDocumentCollaborationIntegration {
|
||||
private var isConnected = false
|
||||
|
||||
override fun initialize() {
|
||||
println("[AurelioVSCodeIntegration] Initializing document collaboration integration")
|
||||
registerCommands()
|
||||
}
|
||||
|
||||
override fun registerCommands() {
|
||||
// Register VSCode commands for document collaboration
|
||||
// This would typically integrate with the VSCode extension API
|
||||
println("[AurelioVSCodeIntegration] Registered document collaboration commands")
|
||||
}
|
||||
|
||||
override fun connectToServices() {
|
||||
if (!isConnected) {
|
||||
runBlocking {
|
||||
val results = documentCollaborationManager.connectAll()
|
||||
isConnected = results.values.any { it }
|
||||
println("[AurelioVSCodeIntegration] Connected to services: $results")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun disconnectFromServices() {
|
||||
isConnected = false
|
||||
println("[AurelioVSCodeIntegration] Disconnected from document collaboration services")
|
||||
}
|
||||
|
||||
override fun uploadFile(filePath: String, serviceName: String): Boolean {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.uploadFile(serviceName, filePath, "/uploaded/${filePath.substringAfterLast("/")}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun downloadFile(remotePath: String, localPath: String, serviceName: String): Boolean {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.downloadFile(serviceName, remotePath, localPath)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createDocument(title: String, content: String, serviceName: String): String? {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.createDocument(serviceName, title, content, "text/plain")
|
||||
}
|
||||
}
|
||||
|
||||
override fun editDocument(documentId: String, content: String, serviceName: String): Boolean {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.editDocument(serviceName, documentId, content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun shareDocument(documentId: String, serviceName: String): String? {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.shareDocument(serviceName, documentId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun listDocuments(serviceName: String): List<DocumentInfo> {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.listDocuments(serviceName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDocumentInfo(documentId: String, serviceName: String): DocumentInfo? {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.getDocumentInfo(serviceName, documentId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aurelio Theia Integration
|
||||
*/
|
||||
class AurelioTheiaIntegration(
|
||||
private val documentCollaborationManager: DocumentCollaborationManager
|
||||
) : PlatformDocumentCollaborationIntegration {
|
||||
private var isConnected = false
|
||||
|
||||
override fun initialize() {
|
||||
println("[AurelioTheiaIntegration] Initializing document collaboration integration")
|
||||
registerCommands()
|
||||
}
|
||||
|
||||
override fun registerCommands() {
|
||||
// Register Theia commands for document collaboration
|
||||
// This would typically integrate with the Theia extension API
|
||||
println("[AurelioTheiaIntegration] Registered document collaboration commands")
|
||||
}
|
||||
|
||||
override fun connectToServices() {
|
||||
if (!isConnected) {
|
||||
runBlocking {
|
||||
val results = documentCollaborationManager.connectAll()
|
||||
isConnected = results.values.any { it }
|
||||
println("[AurelioTheiaIntegration] Connected to services: $results")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun disconnectFromServices() {
|
||||
isConnected = false
|
||||
println("[AurelioTheiaIntegration] Disconnected from document collaboration services")
|
||||
}
|
||||
|
||||
override fun uploadFile(filePath: String, serviceName: String): Boolean {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.uploadFile(serviceName, filePath, "/uploaded/${filePath.substringAfterLast("/")}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun downloadFile(remotePath: String, localPath: String, serviceName: String): Boolean {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.downloadFile(serviceName, remotePath, localPath)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createDocument(title: String, content: String, serviceName: String): String? {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.createDocument(serviceName, title, content, "text/plain")
|
||||
}
|
||||
}
|
||||
|
||||
override fun editDocument(documentId: String, content: String, serviceName: String): Boolean {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.editDocument(serviceName, documentId, content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun shareDocument(documentId: String, serviceName: String): String? {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.shareDocument(serviceName, documentId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun listDocuments(serviceName: String): List<DocumentInfo> {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.listDocuments(serviceName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDocumentInfo(documentId: String, serviceName: String): DocumentInfo? {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.getDocumentInfo(serviceName, documentId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aurelio Web Integration
|
||||
*/
|
||||
class AurelioWebIntegration(
|
||||
private val documentCollaborationManager: DocumentCollaborationManager
|
||||
) : PlatformDocumentCollaborationIntegration {
|
||||
private var isConnected = false
|
||||
|
||||
override fun initialize() {
|
||||
println("[AurelioWebIntegration] Initializing document collaboration integration")
|
||||
}
|
||||
|
||||
override fun registerCommands() {
|
||||
// Register web-based commands for document collaboration
|
||||
// This would typically integrate with the web application's command system
|
||||
println("[AurelioWebIntegration] Registered document collaboration commands")
|
||||
}
|
||||
|
||||
override fun connectToServices() {
|
||||
if (!isConnected) {
|
||||
runBlocking {
|
||||
val results = documentCollaborationManager.connectAll()
|
||||
isConnected = results.values.any { it }
|
||||
println("[AurelioWebIntegration] Connected to services: $results")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun disconnectFromServices() {
|
||||
isConnected = false
|
||||
println("[AurelioWebIntegration] Disconnected from document collaboration services")
|
||||
}
|
||||
|
||||
override fun uploadFile(filePath: String, serviceName: String): Boolean {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.uploadFile(serviceName, filePath, "/uploaded/${filePath.substringAfterLast("/")}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun downloadFile(remotePath: String, localPath: String, serviceName: String): Boolean {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.downloadFile(serviceName, remotePath, localPath)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createDocument(title: String, content: String, serviceName: String): String? {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.createDocument(serviceName, title, content, "text/plain")
|
||||
}
|
||||
}
|
||||
|
||||
override fun editDocument(documentId: String, content: String, serviceName: String): Boolean {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.editDocument(serviceName, documentId, content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun shareDocument(documentId: String, serviceName: String): String? {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.shareDocument(serviceName, documentId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun listDocuments(serviceName: String): List<DocumentInfo> {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.listDocuments(serviceName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDocumentInfo(documentId: String, serviceName: String): DocumentInfo? {
|
||||
if (!isConnected) {
|
||||
connectToServices()
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
documentCollaborationManager.getDocumentInfo(serviceName, documentId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main integration manager that handles all platform integrations
|
||||
*/
|
||||
class PlatformIntegrationManager {
|
||||
private val documentCollaborationManager: DocumentCollaborationManager
|
||||
private val vsCodeIntegration: AurelioVSCodeIntegration
|
||||
private val theiaIntegration: AurelioTheiaIntegration
|
||||
private val webIntegration: AurelioWebIntegration
|
||||
|
||||
init {
|
||||
// Initialize with default configuration
|
||||
val defaultConfig = DocumentCollaborationConfig()
|
||||
documentCollaborationManager = DocumentCollaborationManager(defaultConfig)
|
||||
|
||||
vsCodeIntegration = AurelioVSCodeIntegration(documentCollaborationManager)
|
||||
theiaIntegration = AurelioTheiaIntegration(documentCollaborationManager)
|
||||
webIntegration = AurelioWebIntegration(documentCollaborationManager)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all platform integrations
|
||||
*/
|
||||
fun initializeAll() {
|
||||
vsCodeIntegration.initialize()
|
||||
theiaIntegration.initialize()
|
||||
webIntegration.initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the VSCode integration instance
|
||||
*/
|
||||
fun getVSCodeIntegration(): AurelioVSCodeIntegration {
|
||||
return vsCodeIntegration
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Theia integration instance
|
||||
*/
|
||||
fun getTheiaIntegration(): AurelioTheiaIntegration {
|
||||
return theiaIntegration
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Web integration instance
|
||||
*/
|
||||
fun getWebIntegration(): AurelioWebIntegration {
|
||||
return webIntegration
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to services across all platforms
|
||||
*/
|
||||
suspend fun connectAllPlatforms(): Map<String, Map<String, Boolean>> {
|
||||
val results = mutableMapOf<String, Map<String, Boolean>>()
|
||||
|
||||
results["vscode"] = vsCodeIntegration.let {
|
||||
it.connectToServices()
|
||||
documentCollaborationManager.connectAll()
|
||||
}
|
||||
|
||||
results["theia"] = theiaIntegration.let {
|
||||
it.connectToServices()
|
||||
documentCollaborationManager.connectAll()
|
||||
}
|
||||
|
||||
results["web"] = webIntegration.let {
|
||||
it.connectToServices()
|
||||
documentCollaborationManager.connectAll()
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file across all platforms
|
||||
*/
|
||||
suspend fun uploadFileAcrossPlatforms(filePath: String, serviceName: String): Map<String, Boolean> {
|
||||
val results = mutableMapOf<String, Boolean>()
|
||||
|
||||
results["vscode"] = vsCodeIntegration.uploadFile(filePath, serviceName)
|
||||
results["theia"] = theiaIntegration.uploadFile(filePath, serviceName)
|
||||
results["web"] = webIntegration.uploadFile(filePath, serviceName)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file across all platforms
|
||||
*/
|
||||
suspend fun downloadFileAcrossPlatforms(remotePath: String, localPath: String, serviceName: String): Map<String, Boolean> {
|
||||
val results = mutableMapOf<String, Boolean>()
|
||||
|
||||
results["vscode"] = vsCodeIntegration.downloadFile(remotePath, localPath, serviceName)
|
||||
results["theia"] = theiaIntegration.downloadFile(remotePath, localPath, serviceName)
|
||||
results["web"] = webIntegration.downloadFile(remotePath, localPath, serviceName)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Create document across all platforms
|
||||
*/
|
||||
suspend fun createDocumentAcrossPlatforms(title: String, content: String, serviceName: String): Map<String, String?> {
|
||||
val results = mutableMapOf<String, String?>()
|
||||
|
||||
results["vscode"] = vsCodeIntegration.createDocument(title, content, serviceName)
|
||||
results["theia"] = theiaIntegration.createDocument(title, content, serviceName)
|
||||
results["web"] = webIntegration.createDocument(title, content, serviceName)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit document across all platforms
|
||||
*/
|
||||
suspend fun editDocumentAcrossPlatforms(documentId: String, content: String, serviceName: String): Map<String, Boolean> {
|
||||
val results = mutableMapOf<String, Boolean>()
|
||||
|
||||
results["vscode"] = vsCodeIntegration.editDocument(documentId, content, serviceName)
|
||||
results["theia"] = theiaIntegration.editDocument(documentId, content, serviceName)
|
||||
results["web"] = webIntegration.editDocument(documentId, content, serviceName)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Share document across all platforms
|
||||
*/
|
||||
suspend fun shareDocumentAcrossPlatforms(documentId: String, serviceName: String): Map<String, String?> {
|
||||
val results = mutableMapOf<String, String?>()
|
||||
|
||||
results["vscode"] = vsCodeIntegration.shareDocument(documentId, serviceName)
|
||||
results["theia"] = theiaIntegration.shareDocument(documentId, serviceName)
|
||||
results["web"] = webIntegration.shareDocument(documentId, serviceName)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* List documents across all platforms
|
||||
*/
|
||||
suspend fun listDocumentsAcrossPlatforms(serviceName: String): Map<String, List<DocumentInfo>> {
|
||||
val results = mutableMapOf<String, List<DocumentInfo>>()
|
||||
|
||||
results["vscode"] = vsCodeIntegration.listDocuments(serviceName)
|
||||
results["theia"] = theiaIntegration.listDocuments(serviceName)
|
||||
results["web"] = webIntegration.listDocuments(serviceName)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Get document info across all platforms
|
||||
*/
|
||||
suspend fun getDocumentInfoAcrossPlatforms(documentId: String, serviceName: String): Map<String, DocumentInfo?> {
|
||||
val results = mutableMapOf<String, DocumentInfo?>()
|
||||
|
||||
results["vscode"] = vsCodeIntegration.getDocumentInfo(documentId, serviceName)
|
||||
results["theia"] = theiaIntegration.getDocumentInfo(documentId, serviceName)
|
||||
results["web"] = webIntegration.getDocumentInfo(documentId, serviceName)
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to create a platform integration manager
|
||||
*/
|
||||
fun createPlatformIntegrationManager(): PlatformIntegrationManager {
|
||||
return PlatformIntegrationManager()
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
// Sample integration file for flint-chart in Aurelio VS Code extension
|
||||
// This represents how flint-chart would be integrated into aurelio-vscode
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/**
|
||||
* Flint Chart Integration for Aurelio VS Code Extension
|
||||
* Enables AI agents to generate and display charts directly in chat sessions
|
||||
*/
|
||||
export class FlintChartIntegration {
|
||||
private context: vscode.ExtensionContext;
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this.context = context;
|
||||
this.registerCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers VS Code commands for flint-chart functionality
|
||||
*/
|
||||
private registerCommands(): void {
|
||||
// Command to render a chart from selected data
|
||||
this.context.subscriptions.push(
|
||||
vscode.commands.registerCommand('aurelio.flint-chart.render', async () => {
|
||||
await this.renderChartFromSelection();
|
||||
})
|
||||
);
|
||||
|
||||
// Command to insert chart into chat panel
|
||||
this.context.subscriptions.push(
|
||||
vscode.commands.registerCommand('aurelio.flint-chart.insertInChat', async () => {
|
||||
await this.insertChartInChat();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a chart from selected data in the editor
|
||||
*/
|
||||
private async renderChartFromSelection(): Promise<void> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
vscode.window.showErrorMessage('No active editor found');
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = editor.selection;
|
||||
if (selection.isEmpty) {
|
||||
vscode.window.showWarningMessage('Please select data to chart');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedText = editor.document.getText(selection);
|
||||
const parsedData = this.parseData(selectedText);
|
||||
|
||||
if (!parsedData) {
|
||||
vscode.window.showErrorMessage('Could not parse selected data as chartable data');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and show the chart
|
||||
await this.showChart(parsedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a chart into the chat panel
|
||||
*/
|
||||
private async insertChartInChat(): Promise<void> {
|
||||
// This would communicate with the chat panel to insert a chart
|
||||
// Implementation would depend on the specific chat panel structure
|
||||
vscode.window.showInformationMessage('Inserting chart into chat...');
|
||||
|
||||
// Example: send a message to the webview to render a chart
|
||||
// This would be handled by the chat panel's message passing system
|
||||
const chartData = {
|
||||
type: 'line',
|
||||
data: [10, 20, 30, 40, 50],
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
|
||||
title: 'Sample Chart'
|
||||
};
|
||||
|
||||
// In a real implementation, this would post a message to the chat webview
|
||||
console.log('Would send chart data to chat panel:', chartData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses text data into a format suitable for charting
|
||||
*/
|
||||
private parseData(text: string): any[] | null {
|
||||
try {
|
||||
// Try to parse as JSON first
|
||||
const jsonData = JSON.parse(text);
|
||||
if (Array.isArray(jsonData)) {
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
// Try to parse as CSV-like data
|
||||
const lines = text.trim().split('\n');
|
||||
if (lines.length > 1) {
|
||||
// Simple CSV parsing - assumes numeric values
|
||||
return lines.map(line => {
|
||||
const values = line.split(',').map(item => parseFloat(item.trim()));
|
||||
return values.filter(val => !isNaN(val));
|
||||
}).flat(); // Flatten to single array for now
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, try simpler parsing
|
||||
const numbers = text.match(/[\d.]+/g);
|
||||
if (numbers) {
|
||||
return numbers.map(Number);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a chart preview in a webview panel
|
||||
*/
|
||||
private async showChart(data: any[]): Promise<void> {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'flintChartPreview',
|
||||
'Flint Chart Preview',
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true
|
||||
}
|
||||
);
|
||||
|
||||
panel.webview.html = this.getWebviewContent(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML content for the chart webview
|
||||
*/
|
||||
private getWebviewContent(data: any[]): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Flint Chart</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Chart Preview</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="chartCanvas"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
const ctx = document.getElementById('chartCanvas').getContext('2d');
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ${JSON.stringify(data.map((_, i) => `Item ${i + 1}`))},
|
||||
datasets: [{
|
||||
label: 'Values',
|
||||
data: ${JSON.stringify(data)},
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Generated Chart from Selected Data'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the flint-chart integration
|
||||
*/
|
||||
export function activateFlintChartIntegration(context: vscode.ExtensionContext): FlintChartIntegration {
|
||||
return new FlintChartIntegration(context);
|
||||
}
|
||||
Loading…
Reference in a new issue