From 447540866be1dc97965a12c1f0ffcc50656fc59c Mon Sep 17 00:00:00 2001 From: fabiorafaelcoutada Date: Sun, 12 Jul 2026 20:52:49 +0100 Subject: [PATCH] feat(extensions): aurelio-jetbrains model/config system, document collaboration, flint-chart integration --- .../src/main/kotlin/AgentConfiguration.kt | 303 ++++++ .../src/main/kotlin/AurelioModelSystem.kt | 222 +++++ .../src/main/kotlin/ConfigResolver.kt | 233 +++++ .../src/main/kotlin/ConfigurationManager.kt | 327 +++++++ .../src/main/kotlin/ConfigurationMenu.kt | 655 +++++++++++++ .../DocumentCollaborationConfigSchema.kt | 303 ++++++ .../DocumentCollaborationIntegration.kt | 889 ++++++++++++++++++ .../src/main/kotlin/MixtureFallbackManager.kt | 645 +++++++++++++ .../src/main/kotlin/ModelRouter.kt | 674 +++++++++++++ .../src/main/kotlin/PlatformIntegration.kt | 509 ++++++++++ .../main/resources/flint-chart-integration.ts | 200 ++++ 11 files changed, 4960 insertions(+) create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/AgentConfiguration.kt create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/AurelioModelSystem.kt create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/ConfigResolver.kt create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/ConfigurationManager.kt create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/ConfigurationMenu.kt create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/DocumentCollaborationConfigSchema.kt create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/DocumentCollaborationIntegration.kt create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/MixtureFallbackManager.kt create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/ModelRouter.kt create mode 100644 extensions/aurelio-jetbrains/src/main/kotlin/PlatformIntegration.kt create mode 100644 extensions/aurelio-jetbrains/src/main/resources/flint-chart-integration.ts diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/AgentConfiguration.kt b/extensions/aurelio-jetbrains/src/main/kotlin/AgentConfiguration.kt new file mode 100644 index 00000000..56d19db5 --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/AgentConfiguration.kt @@ -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 +} + +/** + * 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 { + val errors = mutableListOf() + + // 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 + } +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/AurelioModelSystem.kt b/extensions/aurelio-jetbrains/src/main/kotlin/AurelioModelSystem.kt new file mode 100644 index 00000000..ac26b2c8 --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/AurelioModelSystem.kt @@ -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, + 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, + providers: List? = 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, + providers: List? = null, + fallbackStrategy: FallbackStrategy = FallbackStrategy.SEQUENTIAL + ): ModelResult { + val selectedProviders = providers ?: modelRouter.getAvailableProviders() + + return mixtureFallbackManager.executeFallbackRequest( + messages, + selectedProviders, + fallbackStrategy + ) + } + + /** + * Get system statistics + */ + fun getSystemStats(): Map { + 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.") +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/ConfigResolver.kt b/extensions/aurelio-jetbrains/src/main/kotlin/ConfigResolver.kt new file mode 100644 index 00000000..7a92e9ac --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/ConfigResolver.kt @@ -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 = 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(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 { + val errors = mutableListOf() + + 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() +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/ConfigurationManager.kt b/extensions/aurelio-jetbrains/src/main/kotlin/ConfigurationManager.kt new file mode 100644 index 00000000..7c11cb77 --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/ConfigurationManager.kt @@ -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() + 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 { + 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, + val warnings: List +) + +/** + * Global instance of the configuration manager + */ +object GlobalConfigurationManager { + private val manager = ConfigurationManager() + + fun getInstance(): ConfigurationManager = manager +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/ConfigurationMenu.kt b/extensions/aurelio-jetbrains/src/main/kotlin/ConfigurationMenu.kt new file mode 100644 index 00000000..c830d949 --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/ConfigurationMenu.kt @@ -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 { + 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 = 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 +) + +/** + * Configuration schema for UI generation + */ +class ConfigurationSchema { + companion object { + fun getSchema(): List { + 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" + ) + ) + ) + ) + } + } +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/DocumentCollaborationConfigSchema.kt b/extensions/aurelio-jetbrains/src/main/kotlin/DocumentCollaborationConfigSchema.kt new file mode 100644 index 00000000..0c7de9d9 --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/DocumentCollaborationConfigSchema.kt @@ -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 { + 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 { + val baseErrors = baseManager.getValidationErrors() + val docCollabErrors = mutableListOf() + + // 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 +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/DocumentCollaborationIntegration.kt b/extensions/aurelio-jetbrains/src/main/kotlin/DocumentCollaborationIntegration.kt new file mode 100644 index 00000000..b1229746 --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/DocumentCollaborationIntegration.kt @@ -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 + 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 { + 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() + + 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 { + 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 { + 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() + + 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 { + val results = mutableMapOf() + + 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 { + 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 + } +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/MixtureFallbackManager.kt b/extensions/aurelio-jetbrains/src/main/kotlin/MixtureFallbackManager.kt new file mode 100644 index 00000000..8d96e8ed --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/MixtureFallbackManager.kt @@ -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, + 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() + private val mutex = Mutex() + private val config = getConfig() + + /** + * Execute a request with mixture strategy + */ + suspend fun executeMixtureRequest( + messages: List, + providers: List, + 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, + providers: List, + 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, + providers: List + ): MixtureResult { + val responses = mutableListOf() + 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, + providers: List + ): MixtureResult { + val responses = mutableListOf() + + 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, + providers: List + ): MixtureResult { + val responses = mutableListOf() + val successfulContents = mutableListOf() + + 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, + providers: List + ): MixtureResult { + val responses = mutableListOf() + + 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, + providers: List + ): MixtureResult { + val responses = mutableListOf() + + 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, + providers: List + ): 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, + providers: List + ): 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, + providers: List + ): 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, + providers: List + ): 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): 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): 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 { + 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 + } + } + } +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/ModelRouter.kt b/extensions/aurelio-jetbrains/src/main/kotlin/ModelRouter.kt new file mode 100644 index 00000000..bf40c508 --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/ModelRouter.kt @@ -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 = 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() + + // 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, + 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 { + 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): ModelResult { + return callOpenAiCompatibleApi(config, messages, "nvidia-nim") + } + + /** + * Call Ollama API (used by Hermes Agent as fallback) + */ + private suspend fun callOllamaApi(config: ProviderConfig, messages: List): 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, 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): ModelResult { + return callOpenAiCompatibleApi(config, messages, "openai") + } + + /** + * Call Anthropic API + */ + private suspend fun callAnthropicApi(config: ProviderConfig, messages: List): 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( + "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): 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): ModelResult { + return callOpenAiCompatibleApi(config, messages, "mistral") + } + + /** + * Call Together API + */ + private suspend fun callTogetherApi(config: ProviderConfig, messages: List): ModelResult { + return callOpenAiCompatibleApi(config, messages, "together") + } + + /** + * Call LM Studio compatible API + */ + private suspend fun callLmStudioApi(config: ProviderConfig, messages: List): ModelResult { + return callOpenAiCompatibleApi(config, messages, "lmstudio") + } + + /** + * Call custom HTTP API + */ + private suspend fun callCustomHttpApi(config: ProviderConfig, messages: List): 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 { + 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 { + 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() + } + } + } + }) + } +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/kotlin/PlatformIntegration.kt b/extensions/aurelio-jetbrains/src/main/kotlin/PlatformIntegration.kt new file mode 100644 index 00000000..c51008b5 --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/kotlin/PlatformIntegration.kt @@ -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 + 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 { + 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 { + 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 { + 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> { + val results = mutableMapOf>() + + 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 { + val results = mutableMapOf() + + 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 { + val results = mutableMapOf() + + 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 { + val results = mutableMapOf() + + 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 { + val results = mutableMapOf() + + 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 { + val results = mutableMapOf() + + 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> { + val results = mutableMapOf>() + + 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 { + val results = mutableMapOf() + + 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() +} \ No newline at end of file diff --git a/extensions/aurelio-jetbrains/src/main/resources/flint-chart-integration.ts b/extensions/aurelio-jetbrains/src/main/resources/flint-chart-integration.ts new file mode 100644 index 00000000..6408884e --- /dev/null +++ b/extensions/aurelio-jetbrains/src/main/resources/flint-chart-integration.ts @@ -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 { + 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 { + // 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 { + 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 ` + + + + + + Flint Chart + + + + +

Chart Preview

+
+ +
+ + + + `; + } +} + +/** + * Initializes the flint-chart integration + */ +export function activateFlintChartIntegration(context: vscode.ExtensionContext): FlintChartIntegration { + return new FlintChartIntegration(context); +} \ No newline at end of file