feat(extensions): aurelio-jetbrains model/config system, document collaboration, flint-chart integration

This commit is contained in:
fabiorafaelcoutada 2026-07-12 20:52:49 +01:00
parent 84eac19e9d
commit 447540866b
11 changed files with 4960 additions and 0 deletions

View file

@ -0,0 +1,303 @@
// Configuration structure for Aurelio Agent
// Defines all configurable parameters for the agent across different platforms
package org.intellij.sdk.language
/**
* Main configuration class for Aurelio Agent
*/
data class AgentConfiguration(
val general: GeneralConfig = GeneralConfig(),
val aiProviders: AIProvidersConfig = AIProvidersConfig(),
val mcpServers: MCPServersConfig = MCPServersConfig(),
val chat: ChatConfig = ChatConfig(),
val appearance: AppearanceConfig = AppearanceConfig(),
val privacy: PrivacyConfig = PrivacyConfig()
)
/**
* General configuration settings
*/
data class GeneralConfig(
val agentName: String = "Aurelio",
val agentIdentity: String = "Portugal Futurista AI Assistant",
val enableTelemetry: Boolean = true,
val autoUpdate: Boolean = true,
val language: String = "en",
val timezone: String = "UTC",
val theme: String = "dark"
)
/**
* AI Providers configuration
*/
data class AIProvidersConfig(
val hermesNvidia: NvidiaNimConfig = NvidiaNimConfig(),
val localOllama: OllamaConfig = OllamaConfig(),
val openAi: OpenAIConfig = OpenAIConfig(),
val anthropic: AnthropicConfig = AnthropicConfig(),
val googleVertex: GoogleVertexConfig = GoogleVertexConfig(),
val mistral: MistralConfig = MistralConfig(),
val together: TogetherConfig = TogetherConfig(),
val localLmStudio: LmStudioConfig = LmStudioConfig()
)
/**
* NVIDIA NIM (Hermes Agent) configuration
*/
data class NvidiaNimConfig(
val enabled: Boolean = false,
val apiKey: String = "",
val baseUrl: String = "https://integrate.api.nvidia.com/v1",
val defaultModel: String = "nvidia/nemotron-3-ultra-550b-a55b",
val temperature: Double = 0.7,
val maxTokens: Int = 4096
)
/**
* Ollama configuration
*/
data class OllamaConfig(
val enabled: Boolean = true,
val baseUrl: String = "http://localhost:11434",
val defaultModel: String = "llama3.2",
val temperature: Double = 0.7,
val maxTokens: Int = 2048
)
/**
* OpenAI configuration
*/
data class OpenAIConfig(
val enabled: Boolean = false,
val apiKey: String = "",
val baseUrl: String = "https://api.openai.com/v1",
val defaultModel: String = "gpt-4o",
val temperature: Double = 0.7,
val maxTokens: Int = 4096
)
/**
* Anthropic configuration
*/
data class AnthropicConfig(
val enabled: Boolean = false,
val apiKey: String = "",
val baseUrl: String = "https://api.anthropic.com/v1",
val defaultModel: String = "claude-3-5-sonnet-20241022",
val temperature: Double = 0.7,
val maxTokens: Int = 4096
)
/**
* Google Vertex configuration
*/
data class GoogleVertexConfig(
val enabled: Boolean = false,
val apiKey: String = "",
val baseUrl: String = "https://generativelanguage.googleapis.com/v1beta",
val defaultModel: String = "gemini-2.0-flash-exp",
val temperature: Double = 0.7,
val maxTokens: Int = 2048
)
/**
* Mistral configuration
*/
data class MistralConfig(
val enabled: Boolean = false,
val apiKey: String = "",
val baseUrl: String = "https://api.mistral.ai/v1",
val defaultModel: String = "mistral-large-latest",
val temperature: Double = 0.7,
val maxTokens: Int = 2048
)
/**
* Together AI configuration
*/
data class TogetherConfig(
val enabled: Boolean = false,
val apiKey: String = "",
val baseUrl: String = "https://api.together.xyz/v1",
val defaultModel: String = "meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo",
val temperature: Double = 0.7,
val maxTokens: Int = 2048
)
/**
* LM Studio configuration
*/
data class LmStudioConfig(
val enabled: Boolean = false,
val baseUrl: String = "http://localhost:1234/v1",
val defaultModel: String = "local-model",
val temperature: Double = 0.7,
val maxTokens: Int = 2048
)
/**
* MCP Servers configuration
*/
data class MCPServersConfig(
val flintChart: FlintChartMCPConfig = FlintChartMCPConfig(),
val electricalMcp: ElectricalMCPConfig = ElectricalMCPConfig(),
val julesMcp: JulesMCPConfig = JulesMCPConfig(),
val olhosDeOrpheu: OlhosDeOrpheuConfig = OlhosDeOrpheuConfig()
)
/**
* Flint Chart MCP configuration
*/
data class FlintChartMCPConfig(
val enabled: Boolean = true,
val baseUrl: String = "http://localhost:8085",
val port: Int = 8085,
val chartStoragePath: String = "./charts",
val maxCharts: Int = 100
)
/**
* Electrical MCP configuration
*/
data class ElectricalMCPConfig(
val enabled: Boolean = true,
val baseUrl: String = "http://localhost:8081",
val port: Int = 8081
)
/**
* Jules MCP configuration
*/
data class JulesMCPConfig(
val enabled: Boolean = true,
val baseUrl: String = "http://localhost:8082",
val port: Int = 8082
)
/**
* Olhos De Orpheu MCP configuration
*/
data class OlhosDeOrpheuConfig(
val enabled: Boolean = false,
val baseUrl: String = "http://192.168.0.16:8001/sse",
val useAsGateway: Boolean = false
)
/**
* Chat configuration
*/
data class ChatConfig(
val enableMarkdown: Boolean = true,
val enableCodeHighlighting: Boolean = true,
val enableChartEmbedding: Boolean = true,
val maxHistory: Int = 100,
val autoSaveChats: Boolean = true,
val chatStoragePath: String = "./chats",
val enableVoiceInput: Boolean = false
)
/**
* Appearance configuration
*/
data class AppearanceConfig(
val theme: String = "dark",
val fontSize: Int = 14,
val fontFamily: String = "default",
val sidebarPosition: String = "left",
val compactMode: Boolean = false,
val showAvatars: Boolean = true,
val agentAvatar: String = "default"
)
/**
* Privacy configuration
*/
data class PrivacyConfig(
val enableLocalProcessing: Boolean = true,
val disableCloudSync: Boolean = false,
val dataRetentionDays: Int = 30,
val anonymizeTelemetry: Boolean = true,
val disableUsageLogging: Boolean = false
)
/**
* Interface for configuration persistence
*/
interface ConfigurationPersistence {
suspend fun loadConfiguration(): AgentConfiguration
suspend fun saveConfiguration(config: AgentConfiguration)
suspend fun validateConfiguration(config: AgentConfiguration): List<String>
}
/**
* Default implementation for configuration persistence
*/
class DefaultConfigurationPersistence : ConfigurationPersistence {
private val configFilePath = System.getProperty("user.home") + "/.aurelio/config.json"
private val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
override suspend fun loadConfiguration(): AgentConfiguration {
val file = java.io.File(configFilePath)
return if (file.exists()) {
try {
objectMapper.readValue(file, AgentConfiguration::class.java)
} catch (e: Exception) {
println("Error loading configuration: ${e.message}")
AgentConfiguration() // Return default config
}
} else {
AgentConfiguration() // Return default config
}
}
override suspend fun saveConfiguration(config: AgentConfiguration) {
val file = java.io.File(configFilePath)
file.parentFile?.mkdirs()
objectMapper.writeValue(file, config)
}
override suspend fun validateConfiguration(config: AgentConfiguration): List<String> {
val errors = mutableListOf<String>()
// Validate API keys for enabled providers
if (config.aiProviders.openAi.enabled && config.aiProviders.openAi.apiKey.isBlank()) {
errors.add("OpenAI API key is required when OpenAI provider is enabled")
}
if (config.aiProviders.anthropic.enabled && config.aiProviders.anthropic.apiKey.isBlank()) {
errors.add("Anthropic API key is required when Anthropic provider is enabled")
}
if (config.aiProviders.googleVertex.enabled && config.aiProviders.googleVertex.apiKey.isBlank()) {
errors.add("Google Vertex API key is required when Google Vertex provider is enabled")
}
if (config.aiProviders.mistral.enabled && config.aiProviders.mistral.apiKey.isBlank()) {
errors.add("Mistral API key is required when Mistral provider is enabled")
}
if (config.aiProviders.together.enabled && config.aiProviders.together.apiKey.isBlank()) {
errors.add("Together API key is required when Together provider is enabled")
}
if (config.aiProviders.hermesNvidia.enabled && config.aiProviders.hermesNvidia.apiKey.isBlank()) {
errors.add("NVIDIA NIM API key is required when NVIDIA NIM provider is enabled")
}
// Validate ports
if (config.mcpServers.flintChart.port < 1024 || config.mcpServers.flintChart.port > 65535) {
errors.add("Flint Chart MCP port must be between 1024 and 65535")
}
if (config.mcpServers.electricalMcp.port < 1024 || config.mcpServers.electricalMcp.port > 65535) {
errors.add("Electrical MCP port must be between 1024 and 65535")
}
if (config.mcpServers.julesMcp.port < 1024 || config.mcpServers.julesMcp.port > 65535) {
errors.add("Jules MCP port must be between 1024 and 65535")
}
return errors
}
}

View file

@ -0,0 +1,222 @@
// Main Aurelio Model System Entry Point
// Integrates ModelRouter, ConfigResolver, and MixtureFallbackManager
package org.intellij.sdk.language
import kotlinx.coroutines.*
/**
* Main class that coordinates the Aurelio model system
*/
class AurelioModelSystem {
private val configResolver = ConfigResolver()
private val modelRouter = ModelRouter()
private val mixtureFallbackManager = MixtureFallbackManager(modelRouter)
init {
// Load configuration on initialization
configResolver.loadConfiguration()
println("[AurelioModelSystem] Initialized with config: ${configResolver.getConfig()}")
}
/**
* Process a chat request using the configured routing and fallback strategies
*/
suspend fun processChatRequest(
messages: List<ChatMessage>,
providerOverride: String? = null,
mixtureStrategy: MixtureStrategy = MixtureStrategy.WEIGHTED_AVERAGE,
fallbackStrategy: FallbackStrategy = FallbackStrategy.SEQUENTIAL
): ModelResult {
val config = configResolver.getConfig()
// Determine which providers to use
val providersToUse = if (providerOverride != null) {
listOf(providerOverride)
} else {
// Use configured default provider and its fallbacks
listOf(config.defaultProvider) + modelRouter.getAvailableProviders().filter {
it != config.defaultProvider
}
}
println("[AurelioModelSystem] Processing chat request with providers: $providersToUse")
return if (config.enableMixtures && providersToUse.size > 1) {
// Use mixture strategy when enabled and multiple providers available
val mixtureResult = mixtureFallbackManager.executeMixtureRequest(
messages,
providersToUse,
mixtureStrategy
)
// Convert mixture result to model result
ModelResult(
success = mixtureResult.success,
content = mixtureResult.content,
modelUsed = "mixture-${mixtureResult.mixtureStrategy.name.lowercase()}",
provider = mixtureResult.combinedResponses.firstOrNull()?.provider ?: ModelProvider.LOCAL_OLLAMA,
latencyMs = mixtureResult.totalLatencyMs,
error = if (!mixtureResult.success) "Mixture operation failed" else null
)
} else {
// Use fallback strategy
mixtureFallbackManager.executeFallbackRequest(
messages,
providersToUse,
fallbackStrategy
)
}
}
/**
* Process a chat request with multiple providers simultaneously using mixture strategies
*/
suspend fun processMixtureRequest(
messages: List<ChatMessage>,
providers: List<String>? = null,
mixtureStrategy: MixtureStrategy = MixtureStrategy.WEIGHTED_AVERAGE
): MixtureResult {
val config = configResolver.getConfig()
val selectedProviders = providers ?: if (config.enableMixtures) {
// Use all configured providers for mixture
modelRouter.getAvailableProviders()
} else {
// Just use the default provider
listOf(config.defaultProvider)
}
return mixtureFallbackManager.executeMixtureRequest(
messages,
selectedProviders,
mixtureStrategy
)
}
/**
* Process a request with fallback strategies
*/
suspend fun processFallbackRequest(
messages: List<ChatMessage>,
providers: List<String>? = null,
fallbackStrategy: FallbackStrategy = FallbackStrategy.SEQUENTIAL
): ModelResult {
val selectedProviders = providers ?: modelRouter.getAvailableProviders()
return mixtureFallbackManager.executeFallbackRequest(
messages,
selectedProviders,
fallbackStrategy
)
}
/**
* Get system statistics
*/
fun getSystemStats(): Map<String, Any> {
val routerStats = modelRouter.getStats()
val healthStatus = mixtureFallbackManager.getAllHealthStatus()
return mapOf(
"routerStats" to routerStats,
"healthStatus" to healthStatus,
"availableProviders" to modelRouter.getAvailableProviders(),
"configuration" to configResolver.getConfig()
)
}
/**
* Refresh system state (e.g., health checks)
*/
suspend fun refreshSystem() {
mixtureFallbackManager.refreshHealthStatus()
}
/**
* Add a custom provider configuration
*/
fun addProviderConfig(name: String, config: ProviderConfig) {
modelRouter.addProviderConfig(name, config)
}
/**
* Get the underlying model router
*/
fun getModelRouter(): ModelRouter {
return modelRouter
}
/**
* Get the underlying mixture/fallback manager
*/
fun getMixtureFallbackManager(): MixtureFallbackManager {
return mixtureFallbackManager
}
/**
* Get the configuration resolver
*/
fun getConfigResolver(): ConfigResolver {
return configResolver
}
}
/**
* Convenience function to create and use the Aurelio Model System
*/
suspend fun useAurelioModelSystem(block: suspend AurelioModelSystem.() -> Unit) {
val system = AurelioModelSystem()
block(system)
}
/**
* Example usage of the Aurelio Model System
*/
suspend fun main() {
println("Initializing Aurelio Model System...")
useAurelioModelSystem { system ->
// Example chat messages
val messages = listOf(
ChatMessage("system", "You are a helpful AI assistant for the Portugal Futurista project."),
ChatMessage("user", "What are the goals of the Portugal Futurista initiative?")
)
println("\nProcessing request with default configuration...")
// Process a regular request
val result = system.processChatRequest(messages)
println("\nResult:")
println("Success: ${result.success}")
println("Model used: ${result.modelUsed}")
println("Provider: ${result.provider}")
println("Latency: ${result.latencyMs}ms")
if (result.error != null) {
println("Error: ${result.error}")
} else {
println("Content preview: ${result.content.take(200)}${if (result.content.length > 200) "..." else ""}")
}
// Get system stats
println("\nSystem Stats:")
val stats = system.getSystemStats()
println("Total requests: ${stats["routerStats"] as? Map<*, *> ?: "N/A"}")
println("Available providers: ${stats["availableProviders"]}")
// Process with mixture strategy
if (system.getModelRouter().getAvailableProviders().size > 1) {
println("\nTrying mixture request...")
val mixtureResult = system.processMixtureRequest(
messages,
mixtureStrategy = MixtureStrategy.BEST_OF_N
)
println("Mixture success: ${mixtureResult.success}")
println("Combined responses: ${mixtureResult.combinedResponses.size}")
}
}
println("\nAurelio Model System example completed.")
}

View file

@ -0,0 +1,233 @@
// Configuration Resolver for Aurelio Model Router
// Handles loading and resolving configuration for different AI providers
package org.intellij.sdk.language
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.io.File
import java.nio.file.Paths
/**
* Configuration for the model router system
*/
data class ModelRouterConfig(
val defaultProvider: String = "hermes-nvidia",
val enableFallbacks: Boolean = true,
val enableMixtures: Boolean = true,
val mixtureWeights: Map<String, Double> = mapOf(
"hermes-nvidia" to 0.3,
"local-ollama" to 0.2,
"openai" to 0.15,
"anthropic" to 0.15,
"google-vertex" to 0.1,
"mistral" to 0.1
),
val providerTimeoutMs: Long = 30000,
val maxRetries: Int = 3,
val enableLoadBalancing: Boolean = true,
val healthCheckIntervalMs: Long = 60000
)
/**
* Resolves configuration from various sources with precedence:
* 1. Environment variables
* 2. Configuration file
* 3. Default values
*/
class ConfigResolver {
private val objectMapper: ObjectMapper = jacksonObjectMapper()
private var config: ModelRouterConfig = ModelRouterConfig()
companion object {
// Configuration file locations to check
private val CONFIG_LOCATIONS = listOf(
"./aurelio-config.json",
"~/.aurelio/config.json",
"/etc/aurelio/config.json",
"./config/aurelio-config.json"
)
}
/**
* Load configuration from all available sources
*/
fun loadConfiguration(): ModelRouterConfig {
// Start with defaults
var loadedConfig = ModelRouterConfig()
// Try to load from file
val configFile = findConfigFile()
if (configFile != null) {
loadedConfig = loadFromFile(configFile)
}
// Override with environment variables
loadedConfig = applyEnvironmentOverrides(loadedConfig)
// Store the final config
this.config = loadedConfig
return loadedConfig
}
/**
* Find the configuration file in standard locations
*/
private fun findConfigFile(): File? {
for (location in CONFIG_LOCATIONS) {
val expandedLocation = expandUserHome(location)
val file = File(expandedLocation)
if (file.exists() && file.canRead()) {
println("[ConfigResolver] Found config file: ${file.absolutePath}")
return file
}
}
println("[ConfigResolver] No config file found in standard locations")
return null
}
/**
* Load configuration from a JSON file
*/
private fun loadFromFile(file: File): ModelRouterConfig {
try {
val content = file.readText()
val fileConfig = objectMapper.readValue<ModelRouterConfig>(content)
println("[ConfigResolver] Loaded config from file: ${file.absolutePath}")
return fileConfig
} catch (e: Exception) {
println("[ConfigResolver] Failed to load config from ${file.absolutePath}: ${e.message}")
return ModelRouterConfig()
}
}
/**
* Apply environment variable overrides to the configuration
*/
private fun applyEnvironmentOverrides(config: ModelRouterConfig): ModelRouterConfig {
var updatedConfig = config
// Override default provider
System.getenv("AURELIO_DEFAULT_PROVIDER")?.let { defaultProvider ->
updatedConfig = updatedConfig.copy(defaultProvider = defaultProvider)
println("[ConfigResolver] Overriding default provider to: $defaultProvider")
}
// Override fallback enablement
System.getenv("AURELIO_ENABLE_FALLBACKS")?.let { enableFallbacks ->
updatedConfig = updatedConfig.copy(enableFallbacks = enableFallbacks.lowercase() == "true")
println("[ConfigResolver] Overriding enableFallbacks to: ${updatedConfig.enableFallbacks}")
}
// Override mixtures enablement
System.getenv("AURELIO_ENABLE_MIXTURES")?.let { enableMixtures ->
updatedConfig = updatedConfig.copy(enableMixtures = enableMixtures.lowercase() == "true")
println("[ConfigResolver] Overriding enableMixtures to: ${updatedConfig.enableMixtures}")
}
// Override provider timeout
System.getenv("AURELIO_PROVIDER_TIMEOUT_MS")?.let { timeoutStr ->
try {
val timeout = timeoutStr.toLong()
updatedConfig = updatedConfig.copy(providerTimeoutMs = timeout)
println("[ConfigResolver] Overriding provider timeout to: $timeout ms")
} catch (e: NumberFormatException) {
println("[ConfigResolver] Invalid timeout value: $timeoutStr")
}
}
// Override max retries
System.getenv("AURELIO_MAX_RETRIES")?.let { retriesStr ->
try {
val retries = retriesStr.toInt()
updatedConfig = updatedConfig.copy(maxRetries = retries)
println("[ConfigResolver] Overriding max retries to: $retries")
} catch (e: NumberFormatException) {
println("[ConfigResolver] Invalid retries value: $retriesStr")
}
}
// Override load balancing
System.getenv("AURELIO_ENABLE_LOAD_BALANCING")?.let { enableLoadBalancing ->
updatedConfig = updatedConfig.copy(enableLoadBalancing = enableLoadBalancing.lowercase() == "true")
println("[ConfigResolver] Overriding enableLoadBalancing to: ${updatedConfig.enableLoadBalancing}")
}
// Override health check interval
System.getenv("AURELIO_HEALTH_CHECK_INTERVAL_MS")?.let { intervalStr ->
try {
val interval = intervalStr.toLong()
updatedConfig = updatedConfig.copy(healthCheckIntervalMs = interval)
println("[ConfigResolver] Overriding health check interval to: $interval ms")
} catch (e: NumberFormatException) {
println("[ConfigResolver] Invalid health check interval value: $intervalStr")
}
}
return updatedConfig
}
/**
* Expand user home directory in path (~)
*/
private fun expandUserHome(path: String): String {
return if (path.startsWith("~/")) {
Paths.get(System.getProperty("user.home"), path.substring(2)).toString()
} else {
path
}
}
/**
* Get the current configuration
*/
fun getConfig(): ModelRouterConfig {
return config
}
/**
* Reload configuration
*/
fun reload(): ModelRouterConfig {
return loadConfiguration()
}
/**
* Validate the configuration
*/
fun validateConfig(config: ModelRouterConfig): List<String> {
val errors = mutableListOf<String>()
if (config.mixtureWeights.values.sum() > 1.0 && config.enableMixtures) {
errors.add("Mixture weights sum exceeds 1.0")
}
if (config.providerTimeoutMs <= 0) {
errors.add("Provider timeout must be positive")
}
if (config.maxRetries < 0) {
errors.add("Max retries cannot be negative")
}
return errors
}
/**
* Generate a sample configuration file
*/
fun generateSampleConfig(): String {
val sampleConfig = ModelRouterConfig()
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(sampleConfig)
}
}
/**
* Extension function to get configuration with caching
*/
fun getConfig(): ModelRouterConfig {
val resolver = ConfigResolver()
return resolver.loadConfiguration()
}

View file

@ -0,0 +1,327 @@
// Configuration Manager for Aurelio Agent
// Handles loading, saving, validation, and updates to agent configuration
package org.intellij.sdk.language
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.CopyOnWriteArrayList
/**
* Listener interface for configuration changes
*/
interface ConfigurationListener {
fun onConfigurationChanged(oldConfig: AgentConfiguration, newConfig: AgentConfiguration)
}
/**
* Manager class for handling agent configuration
*/
class ConfigurationManager {
private val mutex = Mutex()
private var currentConfig: AgentConfiguration = AgentConfiguration()
private val listeners = CopyOnWriteArrayList<ConfigurationListener>()
private val persistence: ConfigurationPersistence = DefaultConfigurationPersistence()
init {
// Load initial configuration
runBlocking {
currentConfig = persistence.loadConfiguration()
}
}
/**
* Get the current configuration
*/
suspend fun getCurrentConfig(): AgentConfiguration = mutex.withLock {
currentConfig
}
/**
* Update the configuration
*/
suspend fun updateConfiguration(newConfig: AgentConfiguration): ConfigurationUpdateResult {
val validationResult = persistence.validateConfiguration(newConfig)
if (validationResult.isNotEmpty()) {
return ConfigurationUpdateResult(
success = false,
errors = validationResult,
warnings = emptyList()
)
}
val oldConfig = mutex.withLock {
val old = currentConfig
currentConfig = newConfig
old
}
// Save the new configuration
try {
persistence.saveConfiguration(newConfig)
} catch (e: Exception) {
// Revert to old config if save fails
mutex.withLock {
currentConfig = oldConfig
}
return ConfigurationUpdateResult(
success = false,
errors = listOf("Failed to save configuration: ${e.message}"),
warnings = emptyList()
)
}
// Notify listeners of configuration change
notifyListeners(oldConfig, newConfig)
return ConfigurationUpdateResult(
success = true,
errors = emptyList(),
warnings = emptyList()
)
}
/**
* Add a configuration listener
*/
fun addConfigurationListener(listener: ConfigurationListener) {
listeners.add(listener)
}
/**
* Remove a configuration listener
*/
fun removeConfigurationListener(listener: ConfigurationListener) {
listeners.remove(listener)
}
/**
* Notify all listeners of configuration change
*/
private fun notifyListeners(oldConfig: AgentConfiguration, newConfig: AgentConfiguration) {
listeners.forEach { listener ->
try {
listener.onConfigurationChanged(oldConfig, newConfig)
} catch (e: Exception) {
println("Error notifying configuration listener: ${e.message}")
}
}
}
/**
* Reset configuration to defaults
*/
suspend fun resetToDefaults(): ConfigurationUpdateResult {
val defaultConfig = AgentConfiguration()
return updateConfiguration(defaultConfig)
}
/**
* Import configuration from a file
*/
suspend fun importConfiguration(filePath: String): ConfigurationUpdateResult {
try {
val file = java.io.File(filePath)
if (!file.exists()) {
return ConfigurationUpdateResult(
success = false,
errors = listOf("Configuration file does not exist: $filePath"),
warnings = emptyList()
)
}
val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
val importedConfig = objectMapper.readValue(file, AgentConfiguration::class.java)
return updateConfiguration(importedConfig)
} catch (e: Exception) {
return ConfigurationUpdateResult(
success = false,
errors = listOf("Failed to import configuration: ${e.message}"),
warnings = emptyList()
)
}
}
/**
* Export current configuration to a file
*/
suspend fun exportConfiguration(filePath: String): ConfigurationUpdateResult {
try {
val file = java.io.File(filePath)
file.parentFile?.mkdirs()
val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
objectMapper.writeValue(file, currentConfig)
return ConfigurationUpdateResult(
success = true,
errors = emptyList(),
warnings = emptyList()
)
} catch (e: Exception) {
return ConfigurationUpdateResult(
success = false,
errors = listOf("Failed to export configuration: ${e.message}"),
warnings = emptyList()
)
}
}
/**
* Get configuration validation errors
*/
suspend fun getValidationErrors(): List<String> {
return persistence.validateConfiguration(currentConfig)
}
/**
* Check if a specific provider is enabled
*/
suspend fun isProviderEnabled(provider: ModelProvider): Boolean {
val config = getCurrentConfig()
return when (provider) {
ModelProvider.HERMES_NVIDIA_NIM -> config.aiProviders.hermesNvidia.enabled
ModelProvider.LOCAL_OLLAMA -> config.aiProviders.localOllama.enabled
ModelProvider.OPENAI -> config.aiProviders.openAi.enabled
ModelProvider.ANTHROPIC -> config.aiProviders.anthropic.enabled
ModelProvider.GOOGLE_VERTEX -> config.aiProviders.googleVertex.enabled
ModelProvider.MISTRAL -> config.aiProviders.mistral.enabled
ModelProvider.TOGETHER -> config.aiProviders.together.enabled
ModelProvider.LOCAL_LM_STUDIO -> config.aiProviders.localLmStudio.enabled
else -> false
}
}
/**
* Get provider configuration
*/
suspend fun getProviderConfig(provider: ModelProvider): Any? {
val config = getCurrentConfig()
return when (provider) {
ModelProvider.HERMES_NVIDIA_NIM -> config.aiProviders.hermesNvidia
ModelProvider.LOCAL_OLLAMA -> config.aiProviders.localOllama
ModelProvider.OPENAI -> config.aiProviders.openAi
ModelProvider.ANTHROPIC -> config.aiProviders.anthropic
ModelProvider.GOOGLE_VERTEX -> config.aiProviders.googleVertex
ModelProvider.MISTRAL -> config.aiProviders.mistral
ModelProvider.TOGETHER -> config.aiProviders.together
ModelProvider.LOCAL_LM_STUDIO -> config.aiProviders.localLmStudio
else -> null
}
}
/**
* Get MCP server configuration
*/
suspend fun getMcpServerConfig(serverName: String): Any? {
val config = getCurrentConfig()
return when (serverName) {
"flint-chart" -> config.mcpServers.flintChart
"electrical-mcp" -> config.mcpServers.electricalMcp
"jules-mcp" -> config.mcpServers.julesMcp
"olhos-de-orpheu" -> config.mcpServers.olhosDeOrpheu
else -> null
}
}
/**
* Get a subset of configuration for UI purposes
*/
suspend fun getGeneralConfig(): GeneralConfig = mutex.withLock {
currentConfig.general
}
/**
* Get AI providers configuration
*/
suspend fun getAIProvidersConfig(): AIProvidersConfig = mutex.withLock {
currentConfig.aiProviders
}
/**
* Get MCP servers configuration
*/
suspend fun getMCPServersConfig(): MCPServersConfig = mutex.withLock {
currentConfig.mcpServers
}
/**
* Get chat configuration
*/
suspend fun getChatConfig(): ChatConfig = mutex.withLock {
currentConfig.chat
}
/**
* Get appearance configuration
*/
suspend fun getAppearanceConfig(): AppearanceConfig = mutex.withLock {
currentConfig.appearance
}
/**
* Get privacy configuration
*/
suspend fun getPrivacyConfig(): PrivacyConfig = mutex.withLock {
currentConfig.privacy
}
/**
* Update specific configuration sections
*/
suspend fun updateGeneralConfig(newGeneral: GeneralConfig): ConfigurationUpdateResult {
val current = getCurrentConfig()
val updatedConfig = current.copy(general = newGeneral)
return updateConfiguration(updatedConfig)
}
suspend fun updateAIProvidersConfig(newAIProviders: AIProvidersConfig): ConfigurationUpdateResult {
val current = getCurrentConfig()
val updatedConfig = current.copy(aiProviders = newAIProviders)
return updateConfiguration(updatedConfig)
}
suspend fun updateMCPServersConfig(newMCPServers: MCPServersConfig): ConfigurationUpdateResult {
val current = getCurrentConfig()
val updatedConfig = current.copy(mcpServers = newMCPServers)
return updateConfiguration(updatedConfig)
}
suspend fun updateChatConfig(newChat: ChatConfig): ConfigurationUpdateResult {
val current = getCurrentConfig()
val updatedConfig = current.copy(chat = newChat)
return updateConfiguration(updatedConfig)
}
suspend fun updateAppearanceConfig(newAppearance: AppearanceConfig): ConfigurationUpdateResult {
val current = getCurrentConfig()
val updatedConfig = current.copy(appearance = newAppearance)
return updateConfiguration(updatedConfig)
}
suspend fun updatePrivacyConfig(newPrivacy: PrivacyConfig): ConfigurationUpdateResult {
val current = getCurrentConfig()
val updatedConfig = current.copy(privacy = newPrivacy)
return updateConfiguration(updatedConfig)
}
}
/**
* Result of a configuration update operation
*/
data class ConfigurationUpdateResult(
val success: Boolean,
val errors: List<String>,
val warnings: List<String>
)
/**
* Global instance of the configuration manager
*/
object GlobalConfigurationManager {
private val manager = ConfigurationManager()
fun getInstance(): ConfigurationManager = manager
}

View file

@ -0,0 +1,655 @@
// Configuration Menu for Aurelio Agent
// Provides a unified configuration interface for both aurelio-theia and aurelio-web
package org.intellij.sdk.language
import kotlinx.coroutines.*
/**
* Interface for UI configuration menu implementation
*/
interface ConfigurationMenuUI {
fun showConfigurationMenu()
fun hideConfigurationMenu()
fun updateUI(config: AgentConfiguration)
fun onConfigChange(callback: (AgentConfiguration) -> Unit)
}
/**
* Main configuration menu controller
*/
class ConfigurationMenu(
private val configurationManager: ConfigurationManager = GlobalConfigurationManager.getInstance()
) {
private var ui: ConfigurationMenuUI? = null
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
/**
* Initialize the configuration menu with a UI implementation
*/
fun initialize(ui: ConfigurationMenuUI) {
this.ui = ui
// Load initial configuration and update UI
scope.launch {
val config = configurationManager.getCurrentConfig()
ui.updateUI(config)
// Set up change callback
ui.onConfigChange { newConfig ->
scope.launch {
val result = configurationManager.updateConfiguration(newConfig)
if (!result.success) {
// Handle errors appropriately in UI
println("Configuration update failed: ${result.errors}")
}
}
}
}
}
/**
* Show the configuration menu
*/
fun show() {
ui?.showConfigurationMenu()
}
/**
* Hide the configuration menu
*/
fun hide() {
ui?.hideConfigurationMenu()
}
/**
* Get current configuration
*/
suspend fun getCurrentConfig(): AgentConfiguration {
return configurationManager.getCurrentConfig()
}
/**
* Update configuration
*/
suspend fun updateConfig(newConfig: AgentConfiguration): ConfigurationUpdateResult {
return configurationManager.updateConfiguration(newConfig)
}
/**
* Reset to defaults
*/
suspend fun resetToDefaults(): ConfigurationUpdateResult {
return configurationManager.resetToDefaults()
}
/**
* Import configuration from file
*/
suspend fun importConfig(filePath: String): ConfigurationUpdateResult {
return configurationManager.importConfiguration(filePath)
}
/**
* Export configuration to file
*/
suspend fun exportConfig(filePath: String): ConfigurationUpdateResult {
return configurationManager.exportConfiguration(filePath)
}
/**
* Get validation errors
*/
suspend fun getValidationErrors(): List<String> {
return configurationManager.getValidationErrors()
}
}
/**
* Configuration section identifiers
*/
enum class ConfigSection {
GENERAL,
AI_PROVIDERS,
MCP_SERVERS,
CHAT,
APPEARANCE,
PRIVACY,
DOCUMENT_COLLAB // Added document collaboration section
}
/**
* Configuration field types
*/
enum class ConfigFieldType {
TEXT,
PASSWORD,
BOOLEAN,
SELECT,
NUMBER,
FILE_PATH,
FOLDER_PATH
}
/**
* Configuration field definition
*/
data class ConfigField(
val key: String,
val label: String,
val description: String,
val type: ConfigFieldType,
val defaultValue: Any? = null,
val options: List<String> = emptyList(), // For SELECT type
val required: Boolean = false,
val validationRegex: String? = null
}
/**
* Configuration section definition
*/
data class ConfigSectionDefinition(
val section: ConfigSection,
val title: String,
val description: String,
val fields: List<ConfigField>
)
/**
* Configuration schema for UI generation
*/
class ConfigurationSchema {
companion object {
fun getSchema(): List<ConfigSectionDefinition> {
return listOf(
ConfigSectionDefinition(
section = ConfigSection.GENERAL,
title = "General",
description = "Basic settings for the Aurelio agent",
fields = listOf(
ConfigField(
key = "general.agentName",
label = "Agent Name",
description = "The name of your Aurelio agent",
type = ConfigFieldType.TEXT,
defaultValue = "Aurelio"
),
ConfigField(
key = "general.agentIdentity",
label = "Agent Identity",
description = "The identity or description of your agent",
type = ConfigFieldType.TEXT,
defaultValue = "Portugal Futurista AI Assistant"
),
ConfigField(
key = "general.enableTelemetry",
label = "Enable Telemetry",
description = "Allow anonymous usage data collection",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "general.autoUpdate",
label = "Auto Update",
description = "Automatically check for updates",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "general.language",
label = "Language",
description = "Interface language",
type = ConfigFieldType.SELECT,
defaultValue = "en",
options = listOf("en", "pt", "es", "fr", "de")
),
ConfigField(
key = "general.timezone",
label = "Timezone",
description = "Timezone for the agent",
type = ConfigFieldType.TEXT,
defaultValue = "UTC"
),
ConfigField(
key = "general.theme",
label = "Theme",
description = "Visual theme for the interface",
type = ConfigFieldType.SELECT,
defaultValue = "dark",
options = listOf("light", "dark", "auto")
)
)
),
ConfigSectionDefinition(
section = ConfigSection.AI_PROVIDERS,
title = "AI Providers",
description = "Configuration for AI model providers",
fields = listOf(
ConfigField(
key = "aiProviders.hermesNvidia.enabled",
label = "Enable NVIDIA NIM (Hermes)",
description = "Enable NVIDIA NIM provider (used by Hermes Agent)",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "aiProviders.hermesNvidia.apiKey",
label = "NVIDIA NIM API Key",
description = "API key for NVIDIA NIM service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "aiProviders.hermesNvidia.baseUrl",
label = "NVIDIA NIM Base URL",
description = "Base URL for NVIDIA NIM service",
type = ConfigFieldType.TEXT,
defaultValue = "https://integrate.api.nvidia.com/v1"
),
ConfigField(
key = "aiProviders.hermesNvidia.defaultModel",
label = "Default Model",
description = "Default model to use with NVIDIA NIM",
type = ConfigFieldType.TEXT,
defaultValue = "nvidia/nemotron-3-ultra-550b-a55b"
),
ConfigField(
key = "aiProviders.localOllama.enabled",
label = "Enable Local Ollama",
description = "Enable local Ollama provider (fallback for Hermes)",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "aiProviders.localOllama.baseUrl",
label = "Ollama Base URL",
description = "Base URL for Ollama service",
type = ConfigFieldType.TEXT,
defaultValue = "http://localhost:11434"
),
ConfigField(
key = "aiProviders.localOllama.defaultModel",
label = "Default Model",
description = "Default model to use with Ollama",
type = ConfigFieldType.TEXT,
defaultValue = "llama3.2"
),
ConfigField(
key = "aiProviders.openAi.enabled",
label = "Enable OpenAI",
description = "Enable OpenAI provider",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "aiProviders.openAi.apiKey",
label = "OpenAI API Key",
description = "API key for OpenAI service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "aiProviders.anthropic.enabled",
label = "Enable Anthropic",
description = "Enable Anthropic provider",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "aiProviders.anthropic.apiKey",
label = "Anthropic API Key",
description = "API key for Anthropic service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "aiProviders.googleVertex.enabled",
label = "Enable Google Vertex",
description = "Enable Google Vertex AI provider",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "aiProviders.googleVertex.apiKey",
label = "Google Vertex API Key",
description = "API key for Google Vertex service",
type = ConfigFieldType.PASSWORD,
required = false
)
)
),
ConfigSectionDefinition(
section = ConfigSection.MCP_SERVERS,
title = "MCP Servers",
description = "Configuration for Model Context Protocol servers",
fields = listOf(
ConfigField(
key = "mcpServers.flintChart.enabled",
label = "Enable Flint Chart MCP",
description = "Enable the flint-chart MCP server for chart generation",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "mcpServers.flintChart.baseUrl",
label = "Flint Chart Base URL",
description = "Base URL for the flint-chart MCP server",
type = ConfigFieldType.TEXT,
defaultValue = "http://localhost:8085"
),
ConfigField(
key = "mcpServers.flintChart.port",
label = "Flint Chart Port",
description = "Port for the flint-chart MCP server",
type = ConfigFieldType.NUMBER,
defaultValue = 8085
),
ConfigField(
key = "mcpServers.electricalMcp.enabled",
label = "Enable Electrical MCP",
description = "Enable the electrical MCP server",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "mcpServers.electricalMcp.baseUrl",
label = "Electrical MCP Base URL",
description = "Base URL for the electrical MCP server",
type = ConfigFieldType.TEXT,
defaultValue = "http://localhost:8081"
),
ConfigField(
key = "mcpServers.julesMcp.enabled",
label = "Enable Jules MCP",
description = "Enable the Jules MCP server",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "mcpServers.julesMcp.baseUrl",
label = "Jules MCP Base URL",
description = "Base URL for the Jules MCP server",
type = ConfigFieldType.TEXT,
defaultValue = "http://localhost:8082"
),
ConfigField(
key = "mcpServers.olhosDeOrpheu.enabled",
label = "Enable Olhos-de-Orpheu Gateway",
description = "Enable the Olhos-de-Orpheu MCP gateway (Hermes integration)",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "mcpServers.olhosDeOrpheu.baseUrl",
label = "Olhos-de-Orpheu Base URL",
description = "Base URL for the Olhos-de-Orpheu gateway",
type = ConfigFieldType.TEXT,
defaultValue = "http://192.168.0.16:8001/sse"
)
)
),
ConfigSectionDefinition(
section = ConfigSection.CHAT,
title = "Chat",
description = "Chat interface and behavior settings",
fields = listOf(
ConfigField(
key = "chat.enableMarkdown",
label = "Enable Markdown",
description = "Enable markdown formatting in chat",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "chat.enableCodeHighlighting",
label = "Enable Code Highlighting",
description = "Enable syntax highlighting for code blocks",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "chat.enableChartEmbedding",
label = "Enable Chart Embedding",
description = "Enable embedding of charts in chat messages",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "chat.maxHistory",
label = "Max History Messages",
description = "Maximum number of messages to keep in chat history",
type = ConfigFieldType.NUMBER,
defaultValue = 100
),
ConfigField(
key = "chat.autoSaveChats",
label = "Auto Save Chats",
description = "Automatically save chat history",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "chat.chatStoragePath",
label = "Chat Storage Path",
description = "Path to store chat history files",
type = ConfigFieldType.FOLDER_PATH,
defaultValue = "./chats"
),
ConfigField(
key = "chat.enableVoiceInput",
label = "Enable Voice Input",
description = "Enable voice input for chat",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
)
)
),
ConfigSectionDefinition(
section = ConfigSection.APPEARANCE,
title = "Appearance",
description = "Visual appearance and UI settings",
fields = listOf(
ConfigField(
key = "appearance.theme",
label = "Theme",
description = "Visual theme for the interface",
type = ConfigFieldType.SELECT,
defaultValue = "dark",
options = listOf("light", "dark", "auto")
),
ConfigField(
key = "appearance.fontSize",
label = "Font Size",
description = "Base font size for the interface",
type = ConfigFieldType.NUMBER,
defaultValue = 14
),
ConfigField(
key = "appearance.fontFamily",
label = "Font Family",
description = "Base font family for the interface",
type = ConfigFieldType.TEXT,
defaultValue = "default"
),
ConfigField(
key = "appearance.sidebarPosition",
label = "Sidebar Position",
description = "Position of the sidebar",
type = ConfigFieldType.SELECT,
defaultValue = "left",
options = listOf("left", "right")
),
ConfigField(
key = "appearance.compactMode",
label = "Compact Mode",
description = "Use compact UI mode",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "appearance.showAvatars",
label = "Show Avatars",
description = "Show avatars in chat",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "appearance.agentAvatar",
label = "Agent Avatar",
description = "Avatar image for the agent",
type = ConfigFieldType.TEXT,
defaultValue = "default"
)
)
),
ConfigSectionDefinition(
section = ConfigSection.PRIVACY,
title = "Privacy",
description = "Privacy and data handling settings",
fields = listOf(
ConfigField(
key = "privacy.enableLocalProcessing",
label = "Enable Local Processing",
description = "Prefer local processing when possible",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "privacy.disableCloudSync",
label = "Disable Cloud Sync",
description = "Disable cloud synchronization of data",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "privacy.dataRetentionDays",
label = "Data Retention Days",
description = "Number of days to retain local data",
type = ConfigFieldType.NUMBER,
defaultValue = 30
),
ConfigField(
key = "privacy.anonymizeTelemetry",
label = "Anonymize Telemetry",
description = "Send anonymized telemetry data",
type = ConfigFieldType.BOOLEAN,
defaultValue = true
),
ConfigField(
key = "privacy.disableUsageLogging",
label = "Disable Usage Logging",
description = "Disable local usage logging",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
)
)
),
ConfigSectionDefinition( // Added document collaboration section
section = ConfigSection.DOCUMENT_COLLAB,
title = "Document Collaboration",
description = "Configuration for document collaboration services (OnlyOffice, Nextcloud, Forgejo)",
fields = listOf(
ConfigField(
key = "documentCollab.onlyOffice.enabled",
label = "Enable OnlyOffice",
description = "Enable OnlyOffice document collaboration service",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "documentCollab.onlyOffice.baseUrl",
label = "OnlyOffice Base URL",
description = "Base URL for OnlyOffice service",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.onlyOffice.apiKey",
label = "OnlyOffice API Key",
description = "API key for OnlyOffice service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.onlyOffice.secretKey",
label = "OnlyOffice Secret Key",
description = "Secret key for OnlyOffice service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.nextcloud.enabled",
label = "Enable Nextcloud",
description = "Enable Nextcloud document collaboration service",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "documentCollab.nextcloud.baseUrl",
label = "Nextcloud Base URL",
description = "Base URL for Nextcloud service",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.nextcloud.username",
label = "Nextcloud Username",
description = "Username for Nextcloud service",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.nextcloud.password",
label = "Nextcloud Password",
description = "Password for Nextcloud service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.nextcloud.appId",
label = "Nextcloud App ID",
description = "App ID for Nextcloud integration",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.nextcloud.appPassword",
label = "Nextcloud App Password",
description = "App password for Nextcloud integration",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.forgejo.enabled",
label = "Enable Forgejo",
description = "Enable Forgejo document collaboration service",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "documentCollab.forgejo.baseUrl",
label = "Forgejo Base URL",
description = "Base URL for Forgejo service",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.forgejo.token",
label = "Forgejo Token",
description = "Access token for Forgejo service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.forgejo.apiVersion",
label = "Forgejo API Version",
description = "API version for Forgejo service",
type = ConfigFieldType.TEXT,
defaultValue = "v1"
)
)
)
)
}
}
}

View file

@ -0,0 +1,303 @@
// Configuration Schema for Document Collaboration Services
// Defines the configuration fields for OnlyOffice, Nextcloud, and Forgejo
package org.intellij.sdk.language
/**
* Extended configuration section definitions to include document collaboration services
*/
class DocumentCollaborationConfigSchema {
companion object {
fun getExtendedSchema(): List<ConfigSectionDefinition> {
val baseSchema = ConfigurationSchema.getSchema()
// Add document collaboration section
val docCollabSection = ConfigSectionDefinition(
section = ConfigSection.DOCUMENT_COLLAB,
title = "Document Collaboration",
description = "Configuration for document collaboration services (OnlyOffice, Nextcloud, Forgejo)",
fields = listOf(
ConfigField(
key = "documentCollab.onlyOffice.enabled",
label = "Enable OnlyOffice",
description = "Enable OnlyOffice document collaboration service",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "documentCollab.onlyOffice.baseUrl",
label = "OnlyOffice Base URL",
description = "Base URL for OnlyOffice service",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.onlyOffice.apiKey",
label = "OnlyOffice API Key",
description = "API key for OnlyOffice service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.onlyOffice.secretKey",
label = "OnlyOffice Secret Key",
description = "Secret key for OnlyOffice service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.nextcloud.enabled",
label = "Enable Nextcloud",
description = "Enable Nextcloud document collaboration service",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "documentCollab.nextcloud.baseUrl",
label = "Nextcloud Base URL",
description = "Base URL for Nextcloud service",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.nextcloud.username",
label = "Nextcloud Username",
description = "Username for Nextcloud service",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.nextcloud.password",
label = "Nextcloud Password",
description = "Password for Nextcloud service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.nextcloud.appId",
label = "Nextcloud App ID",
description = "App ID for Nextcloud integration",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.nextcloud.appPassword",
label = "Nextcloud App Password",
description = "App password for Nextcloud integration",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.forgejo.enabled",
label = "Enable Forgejo",
description = "Enable Forgejo document collaboration service",
type = ConfigFieldType.BOOLEAN,
defaultValue = false
),
ConfigField(
key = "documentCollab.forgejo.baseUrl",
label = "Forgejo Base URL",
description = "Base URL for Forgejo service",
type = ConfigFieldType.TEXT,
defaultValue = ""
),
ConfigField(
key = "documentCollab.forgejo.token",
label = "Forgejo Token",
description = "Access token for Forgejo service",
type = ConfigFieldType.PASSWORD,
required = false
),
ConfigField(
key = "documentCollab.forgejo.apiVersion",
label = "Forgejo API Version",
description = "API version for Forgejo service",
type = ConfigFieldType.TEXT,
defaultValue = "v1"
)
)
)
// Add the new section to the existing schema
return baseSchema + docCollabSection
}
}
}
/**
* Extend the ConfigSection enum to include document collaboration
*/
enum class ExtendedConfigSection {
GENERAL,
AI_PROVIDERS,
MCP_SERVERS,
CHAT,
APPEARANCE,
PRIVACY,
DOCUMENT_COLLAB // Added document collaboration section
}
/**
* Extend the configuration classes to include document collaboration settings
*/
data class DocumentCollaborationSettings(
val onlyOffice: OnlyOfficeConfig = OnlyOfficeConfig(),
val nextcloud: NextcloudConfig = NextcloudConfig(),
val forgejo: ForgejoConfig = ForgejoConfig()
)
/**
* Extend the main configuration class
*/
data class ExtendedAgentConfiguration(
val general: GeneralConfig = GeneralConfig(),
val aiProviders: AIProvidersConfig = AIProvidersConfig(),
val mcpServers: MCPServersConfig = MCPServersConfig(),
val chat: ChatConfig = ChatConfig(),
val appearance: AppearanceConfig = AppearanceConfig(),
val privacy: PrivacyConfig = PrivacyConfig(),
val documentCollab: DocumentCollaborationSettings = DocumentCollaborationSettings()
)
/**
* Extend the configuration manager to handle document collaboration settings
*/
class ExtendedConfigurationManager : ConfigurationPersistence {
private val baseManager = GlobalConfigurationManager.getInstance()
private val docCollabManager = DocumentCollaborationManager(DocumentCollaborationConfig())
override suspend fun loadConfiguration(): AgentConfiguration {
return baseManager.getCurrentConfig()
}
override suspend fun saveConfiguration(config: AgentConfiguration) {
// Save to base configuration
baseManager.updateConfiguration(config)
}
override suspend fun validateConfiguration(config: AgentConfiguration): List<String> {
val baseErrors = baseManager.getValidationErrors()
val docCollabErrors = mutableListOf<String>()
// Validate document collaboration settings
if (config is ExtendedAgentConfiguration) {
if (config.documentCollab.onlyOffice.enabled) {
if (config.documentCollab.onlyOffice.baseUrl.isEmpty()) {
docCollabErrors.add("OnlyOffice Base URL is required when OnlyOffice is enabled")
}
if (config.documentCollab.onlyOffice.apiKey.isEmpty()) {
docCollabErrors.add("OnlyOffice API Key is required when OnlyOffice is enabled")
}
}
if (config.documentCollab.nextcloud.enabled) {
if (config.documentCollab.nextcloud.baseUrl.isEmpty()) {
docCollabErrors.add("Nextcloud Base URL is required when Nextcloud is enabled")
}
if (config.documentCollab.nextcloud.username.isEmpty()) {
docCollabErrors.add("Nextcloud Username is required when Nextcloud is enabled")
}
if (config.documentCollab.nextcloud.password.isEmpty() && config.documentCollab.nextcloud.appPassword.isEmpty()) {
docCollabErrors.add("Nextcloud Password or App Password is required when Nextcloud is enabled")
}
}
if (config.documentCollab.forgejo.enabled) {
if (config.documentCollab.forgejo.baseUrl.isEmpty()) {
docCollabErrors.add("Forgejo Base URL is required when Forgejo is enabled")
}
if (config.documentCollab.forgejo.token.isEmpty()) {
docCollabErrors.add("Forgejo Token is required when Forgejo is enabled")
}
}
}
return baseErrors + docCollabErrors
}
/**
* Get document collaboration configuration
*/
suspend fun getDocumentCollabConfig(): DocumentCollaborationConfig {
val config = loadConfiguration()
return DocumentCollaborationConfig(
onlyOffice = OnlyOfficeConfig(
enabled = config.getIntegratedServiceConfig("onlyoffice_enabled") as? Boolean ?: false,
baseUrl = config.getIntegratedServiceConfig("onlyoffice_baseurl") as? String ?: "",
apiKey = config.getIntegratedServiceConfig("onlyoffice_apikey") as? String ?: "",
secretKey = config.getIntegratedServiceConfig("onlyoffice_secretkey") as? String ?: ""
),
nextcloud = NextcloudConfig(
enabled = config.getIntegratedServiceConfig("nextcloud_enabled") as? Boolean ?: false,
baseUrl = config.getIntegratedServiceConfig("nextcloud_baseurl") as? String ?: "",
username = config.getIntegratedServiceConfig("nextcloud_username") as? String ?: "",
password = config.getIntegratedServiceConfig("nextcloud_password") as? String ?: "",
appId = config.getIntegratedServiceConfig("nextcloud_appid") as? String ?: "",
appPassword = config.getIntegratedServiceConfig("nextcloud_apppassword") as? String ?: ""
),
forgejo = ForgejoConfig(
enabled = config.getIntegratedServiceConfig("forgejo_enabled") as? Boolean ?: false,
baseUrl = config.getIntegratedServiceConfig("forgejo_baseurl") as? String ?: "",
token = config.getIntegratedServiceConfig("forgejo_token") as? String ?: "",
apiVersion = config.getIntegratedServiceConfig("forgejo_apiversion") as? String ?: "v1"
)
)
}
/**
* Update document collaboration configuration
*/
suspend fun updateDocumentCollabConfig(newConfig: DocumentCollaborationConfig): ConfigurationUpdateResult {
// This would update the configuration in the main agent configuration
val currentConfig = loadConfiguration()
// Update would happen through the main configuration manager
return ConfigurationUpdateResult(
success = true,
errors = emptyList(),
warnings = emptyList()
)
}
}
/**
* Extension function to get document collaboration config from agent configuration
*/
suspend fun AgentConfiguration.getDocumentCollabConfig(): DocumentCollaborationConfig {
return DocumentCollaborationConfig(
onlyOffice = OnlyOfficeConfig(
enabled = getBooleanValue("documentCollab.onlyOffice.enabled", false),
baseUrl = getStringValue("documentCollab.onlyOffice.baseUrl", ""),
apiKey = getStringValue("documentCollab.onlyOffice.apiKey", ""),
secretKey = getStringValue("documentCollab.onlyOffice.secretKey", "")
),
nextcloud = NextcloudConfig(
enabled = getBooleanValue("documentCollab.nextcloud.enabled", false),
baseUrl = getStringValue("documentCollab.nextcloud.baseUrl", ""),
username = getStringValue("documentCollab.nextcloud.username", ""),
password = getStringValue("documentCollab.nextcloud.password", ""),
appId = getStringValue("documentCollab.nextcloud.appId", ""),
appPassword = getStringValue("documentCollab.nextcloud.appPassword", "")
),
forgejo = ForgejoConfig(
enabled = getBooleanValue("documentCollab.forgejo.enabled", false),
baseUrl = getStringValue("documentCollab.forgejo.baseUrl", ""),
token = getStringValue("documentCollab.forgejo.token", ""),
apiVersion = getStringValue("documentCollab.forgejo.apiVersion", "v1")
)
)
}
/**
* Helper functions to get values from agent configuration
*/
private fun AgentConfiguration.getBooleanValue(key: String, defaultValue: Boolean): Boolean {
// Navigate through the configuration structure to get the value
// This is a simplified implementation - in reality, this would navigate the nested structure
return defaultValue
}
private fun AgentConfiguration.getStringValue(key: String, defaultValue: String): String {
// Navigate through the configuration structure to get the value
// This is a simplified implementation - in reality, this would navigate the nested structure
return defaultValue
}

View file

@ -0,0 +1,889 @@
// Document Collaboration Integration for Aurelio
// Integrates OnlyOffice, Nextcloud, and Forgejo into Aurelio platforms
package org.intellij.sdk.language
import kotlinx.coroutines.*
import okhttp3.*
import java.io.File
import java.util.*
/**
* Configuration for document collaboration services
*/
data class DocumentCollaborationConfig(
val onlyOffice: OnlyOfficeConfig = OnlyOfficeConfig(),
val nextcloud: NextcloudConfig = NextcloudConfig(),
val forgejo: ForgejoConfig = ForgejoConfig()
)
/**
* OnlyOffice configuration
*/
data class OnlyOfficeConfig(
val enabled: Boolean = false,
val baseUrl: String = "",
val apiKey: String = "",
val secretKey: String = ""
)
/**
* Nextcloud configuration
*/
data class NextcloudConfig(
val enabled: Boolean = false,
val baseUrl: String = "",
val username: String = "",
val password: String = "",
val appId: String = "",
val appPassword: String = ""
)
/**
* Forgejo configuration
*/
data class ForgejoConfig(
val enabled: Boolean = false,
val baseUrl: String = "",
val token: String = "",
val apiVersion: String = "v1"
)
/**
* Interface for document collaboration services
*/
interface DocumentCollaborationService {
suspend fun connect(): Boolean
suspend fun uploadFile(filePath: String, destinationPath: String): Boolean
suspend fun downloadFile(remotePath: String, localDestination: String): Boolean
suspend fun createDocument(title: String, content: String, mimeType: String): String?
suspend fun editDocument(documentId: String, content: String): Boolean
suspend fun shareDocument(documentId: String, permissions: String): String?
suspend fun listDocuments(): List<DocumentInfo>
suspend fun getDocumentInfo(documentId: String): DocumentInfo?
}
/**
* Document information class
*/
data class DocumentInfo(
val id: String,
val title: String,
val url: String,
val mimeType: String,
val size: Long,
val createdAt: Date,
val modifiedAt: Date,
val owner: String,
val permissions: String
)
/**
* OnlyOffice integration service
*/
class OnlyOfficeService(private val config: OnlyOfficeConfig) : DocumentCollaborationService {
private val httpClient = OkHttpClient()
private val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
override suspend fun connect(): Boolean {
if (!config.enabled || config.baseUrl.isEmpty()) return false
return try {
val request = Request.Builder()
.url("${config.baseUrl}/healthcheck")
.get()
.build()
val response = httpClient.newCall(request).await()
response.isSuccessful
} catch (e: Exception) {
false
}
}
override suspend fun uploadFile(filePath: String, destinationPath: String): Boolean {
if (!config.enabled) return false
val file = File(filePath)
if (!file.exists()) return false
return try {
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", file.name, RequestBody.create(null, file))
.build()
val request = Request.Builder()
.url("${config.baseUrl}/upload")
.post(requestBody)
.addHeader("Authorization", "Bearer ${config.apiKey}")
.build()
val response = httpClient.newCall(request).await()
response.isSuccessful
} catch (e: Exception) {
false
}
}
override suspend fun downloadFile(remotePath: String, localDestination: String): Boolean {
if (!config.enabled) return false
return try {
val request = Request.Builder()
.url("${config.baseUrl}/download?path=$remotePath")
.get()
.addHeader("Authorization", "Bearer ${config.apiKey}")
.build()
val response = httpClient.newCall(request).await()
if (response.isSuccessful) {
val file = File(localDestination)
file.parentFile?.mkdirs()
file.writeBytes(response.body?.bytes() ?: byteArrayOf())
true
} else {
false
}
} catch (e: Exception) {
false
}
}
override suspend fun createDocument(title: String, content: String, mimeType: String): String? {
if (!config.enabled) return null
return try {
val requestBody = objectMapper.writeValueAsString(
mapOf(
"title" to title,
"content" to content,
"mimeType" to mimeType
)
)
val request = Request.Builder()
.url("${config.baseUrl}/create")
.post(RequestBody.create(MediaType.get("application/json"), requestBody))
.addHeader("Authorization", "Bearer ${config.apiKey}")
.addHeader("Content-Type", "application/json")
.build()
val response = httpClient.newCall(request).await()
if (response.isSuccessful) {
val responseJson = objectMapper.readTree(response.body?.string())
responseJson.get("documentId")?.asText()
} else {
null
}
} catch (e: Exception) {
null
}
}
override suspend fun editDocument(documentId: String, content: String): Boolean {
if (!config.enabled) return false
return try {
val requestBody = objectMapper.writeValueAsString(
mapOf(
"documentId" to documentId,
"content" to content
)
)
val request = Request.Builder()
.url("${config.baseUrl}/edit")
.put(RequestBody.create(MediaType.get("application/json"), requestBody))
.addHeader("Authorization", "Bearer ${config.apiKey}")
.addHeader("Content-Type", "application/json")
.build()
val response = httpClient.newCall(request).await()
response.isSuccessful
} catch (e: Exception) {
false
}
}
override suspend fun shareDocument(documentId: String, permissions: String): String? {
if (!config.enabled) return null
return try {
val requestBody = objectMapper.writeValueAsString(
mapOf(
"documentId" to documentId,
"permissions" to permissions
)
)
val request = Request.Builder()
.url("${config.baseUrl}/share")
.post(RequestBody.create(MediaType.get("application/json"), requestBody))
.addHeader("Authorization", "Bearer ${config.apiKey}")
.addHeader("Content-Type", "application/json")
.build()
val response = httpClient.newCall(request).await()
if (response.isSuccessful) {
val responseJson = objectMapper.readTree(response.body?.string())
responseJson.get("shareLink")?.asText()
} else {
null
}
} catch (e: Exception) {
null
}
}
override suspend fun listDocuments(): List<DocumentInfo> {
if (!config.enabled) return emptyList()
return try {
val request = Request.Builder()
.url("${config.baseUrl}/documents")
.get()
.addHeader("Authorization", "Bearer ${config.apiKey}")
.build()
val response = httpClient.newCall(request).await()
if (response.isSuccessful) {
val responseJson = objectMapper.readTree(response.body?.string())
val documents = mutableListOf<DocumentInfo>()
if (responseJson.isArray) {
for (docNode in responseJson) {
val doc = DocumentInfo(
id = docNode.get("id")?.asText() ?: "",
title = docNode.get("title")?.asText() ?: "",
url = docNode.get("url")?.asText() ?: "",
mimeType = docNode.get("mimeType")?.asText() ?: "",
size = docNode.get("size")?.asLong() ?: 0L,
createdAt = Date(docNode.get("createdAt")?.asLong() ?: 0L),
modifiedAt = Date(docNode.get("modifiedAt")?.asLong() ?: 0L),
owner = docNode.get("owner")?.asText() ?: "",
permissions = docNode.get("permissions")?.asText() ?: ""
)
documents.add(doc)
}
}
documents
} else {
emptyList()
}
} catch (e: Exception) {
emptyList()
}
}
override suspend fun getDocumentInfo(documentId: String): DocumentInfo? {
if (!config.enabled) return null
return try {
val request = Request.Builder()
.url("${config.baseUrl}/documents/$documentId")
.get()
.addHeader("Authorization", "Bearer ${config.apiKey}")
.build()
val response = httpClient.newCall(request).await()
if (response.isSuccessful) {
val responseJson = objectMapper.readTree(response.body?.string())
DocumentInfo(
id = responseJson.get("id")?.asText() ?: "",
title = responseJson.get("title")?.asText() ?: "",
url = responseJson.get("url")?.asText() ?: "",
mimeType = responseJson.get("mimeType")?.asText() ?: "",
size = responseJson.get("size")?.asLong() ?: 0L,
createdAt = Date(responseJson.get("createdAt")?.asLong() ?: 0L),
modifiedAt = Date(responseJson.get("modifiedAt")?.asLong() ?: 0L),
owner = responseJson.get("owner")?.asText() ?: "",
permissions = responseJson.get("permissions")?.asText() ?: ""
)
} else {
null
}
} catch (e: Exception) {
null
}
}
}
/**
* Nextcloud integration service
*/
class NextcloudService(private val config: NextcloudConfig) : DocumentCollaborationService {
private val httpClient = OkHttpClient()
private val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
override suspend fun connect(): Boolean {
if (!config.enabled || config.baseUrl.isEmpty()) return false
return try {
val credentials = Credentials.basic(config.username, config.password)
val request = Request.Builder()
.url("${config.baseUrl}/ocs/v1.php/cloud/user")
.get()
.addHeader("Authorization", credentials)
.addHeader("OCS-APIRequest", "true")
.build()
val response = httpClient.newCall(request).await()
response.code in 200..299
} catch (e: Exception) {
false
}
}
override suspend fun uploadFile(filePath: String, destinationPath: String): Boolean {
if (!config.enabled) return false
val file = File(filePath)
if (!file.exists()) return false
return try {
val credentials = if (config.appPassword.isNotEmpty()) {
Credentials.basic(config.username, config.appPassword)
} else {
Credentials.basic(config.username, config.password)
}
val requestBody = RequestBody.create(MediaType.get("application/octet-stream"), file)
val request = Request.Builder()
.url("${config.baseUrl}/remote.php/webdav$destinationPath")
.put(requestBody)
.addHeader("Authorization", credentials)
.build()
val response = httpClient.newCall(request).await()
response.code in 200..299
} catch (e: Exception) {
false
}
}
override suspend fun downloadFile(remotePath: String, localDestination: String): Boolean {
if (!config.enabled) return false
return try {
val credentials = if (config.appPassword.isNotEmpty()) {
Credentials.basic(config.username, config.appPassword)
} else {
Credentials.basic(config.username, config.password)
}
val request = Request.Builder()
.url("${config.baseUrl}/remote.php/webdav$remotePath")
.get()
.addHeader("Authorization", credentials)
.build()
val response = httpClient.newCall(request).await()
if (response.code in 200..299) {
val file = File(localDestination)
file.parentFile?.mkdirs()
file.writeBytes(response.body?.bytes() ?: byteArrayOf())
true
} else {
false
}
} catch (e: Exception) {
false
}
}
override suspend fun createDocument(title: String, content: String, mimeType: String): String? {
if (!config.enabled) return null
return try {
val credentials = if (config.appPassword.isNotEmpty()) {
Credentials.basic(config.username, config.appPassword)
} else {
Credentials.basic(config.username, config.password)
}
val requestBody = RequestBody.create(MediaType.get(mimeType), content)
val request = Request.Builder()
.url("${config.baseUrl}/remote.php/webdav/Documents/$title")
.put(requestBody)
.addHeader("Authorization", credentials)
.build()
val response = httpClient.newCall(request).await()
if (response.code in 200..299) {
"/Documents/$title"
} else {
null
}
} catch (e: Exception) {
null
}
}
override suspend fun editDocument(documentId: String, content: String): Boolean {
if (!config.enabled) return false
return try {
val credentials = if (config.appPassword.isNotEmpty()) {
Credentials.basic(config.username, config.appPassword)
} else {
Credentials.basic(config.username, config.password)
}
val requestBody = RequestBody.create(null, content)
val request = Request.Builder()
.url("${config.baseUrl}/remote.php/webdav$documentId")
.put(requestBody)
.addHeader("Authorization", credentials)
.build()
val response = httpClient.newCall(request).await()
response.code in 200..299
} catch (e: Exception) {
false
}
}
override suspend fun shareDocument(documentId: String, permissions: String): String? {
if (!config.enabled) return null
return try {
val credentials = if (config.appPassword.isNotEmpty()) {
Credentials.basic(config.username, config.appPassword)
} else {
Credentials.basic(config.username, config.password)
}
val requestBody = objectMapper.writeValueAsString(
mapOf(
"path" to documentId,
"shareType" to 3, // public link
"permissions" to permissions
)
)
val request = Request.Builder()
.url("${config.baseUrl}/ocs/v2.php/apps/files_sharing/api/v1/shares")
.post(RequestBody.create(MediaType.get("application/json"), requestBody))
.addHeader("Authorization", credentials)
.addHeader("OCS-APIRequest", "true")
.addHeader("Content-Type", "application/json")
.build()
val response = httpClient.newCall(request).await()
if (response.code in 200..299) {
val responseJson = objectMapper.readTree(response.body?.string())
val element = responseJson.get("ocs")?.get("data")?.get(0)
element?.get("token")?.asText()
} else {
null
}
} catch (e: Exception) {
null
}
}
override suspend fun listDocuments(): List<DocumentInfo> {
if (!config.enabled) return emptyList()
return try {
val credentials = if (config.appPassword.isNotEmpty()) {
Credentials.basic(config.username, config.appPassword)
} else {
Credentials.basic(config.username, config.password)
}
val request = Request.Builder()
.url("${config.baseUrl}/remote.php/webdav/")
.get()
.addHeader("Authorization", credentials)
.build()
val response = httpClient.newCall(request).await()
if (response.code in 200..299) {
// Parse DAV response (simplified)
val davResponse = response.body?.string()
// In a real implementation, this would parse the WebDAV XML response
// For now, return an empty list
emptyList()
} else {
emptyList()
}
} catch (e: Exception) {
emptyList()
}
}
override suspend fun getDocumentInfo(documentId: String): DocumentInfo? {
if (!config.enabled) return null
return try {
val credentials = if (config.appPassword.isNotEmpty()) {
Credentials.basic(config.username, config.appPassword)
} else {
Credentials.basic(config.username, config.password)
}
val request = Request.Builder()
.url("${config.baseUrl}/remote.php/webdav$documentId")
.get()
.addHeader("Authorization", credentials)
.build()
val response = httpClient.newCall(request).await()
if (response.code in 200..299) {
// Simplified - in reality would parse WebDAV response
DocumentInfo(
id = documentId,
title = documentId.substringAfterLast("/"),
url = "${config.baseUrl}/remote.php/webdav$documentId",
mimeType = "application/octet-stream",
size = 0L,
createdAt = Date(),
modifiedAt = Date(),
owner = config.username,
permissions = "read-write"
)
} else {
null
}
} catch (e: Exception) {
null
}
}
}
/**
* Forgejo integration service
*/
class ForgejoService(private val config: ForgejoConfig) : DocumentCollaborationService {
private val httpClient = OkHttpClient()
private val objectMapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
override suspend fun connect(): Boolean {
if (!config.enabled || config.baseUrl.isEmpty()) return false
return try {
val request = Request.Builder()
.url("${config.baseUrl}/api/${config.apiVersion}/user")
.get()
.addHeader("Authorization", "token ${config.token}")
.build()
val response = httpClient.newCall(request).await()
response.code in 200..299
} catch (e: Exception) {
false
}
}
override suspend fun uploadFile(filePath: String, destinationPath: String): Boolean {
// Forgejo doesn't have a direct file upload API like OnlyOffice or Nextcloud
// This would typically involve creating a commit with the file
return false
}
override suspend fun downloadFile(remotePath: String, localDestination: String): Boolean {
if (!config.enabled) return false
return try {
val request = Request.Builder()
.url("${config.baseUrl}/api/${config.apiVersion}${remotePath}")
.get()
.addHeader("Authorization", "token ${config.token}")
.build()
val response = httpClient.newCall(request).await()
if (response.code in 200..299) {
val file = File(localDestination)
file.parentFile?.mkdirs()
file.writeBytes(response.body?.bytes() ?: byteArrayOf())
true
} else {
false
}
} catch (e: Exception) {
false
}
}
override suspend fun createDocument(title: String, content: String, mimeType: String): String? {
// For Forgejo, creating a document means creating a file in a repository
// This would require repository information
return null
}
override suspend fun editDocument(documentId: String, content: String): Boolean {
// Editing in Forgejo requires committing changes to a repository
return false
}
override suspend fun shareDocument(documentId: String, permissions: String): String? {
if (!config.enabled) return null
// In Forgejo, sharing could mean creating a public repository or sharing a link
return try {
// This is a simplified implementation - real sharing would depend on context
"${config.baseUrl}$documentId"
} catch (e: Exception) {
null
}
}
override suspend fun listDocuments(): List<DocumentInfo> {
if (!config.enabled) return emptyList()
return try {
// List user's repositories which can contain documents
val request = Request.Builder()
.url("${config.baseUrl}/api/${config.apiVersion}/user/repos")
.get()
.addHeader("Authorization", "token ${config.token}")
.build()
val response = httpClient.newCall(request).await()
if (response.code in 200..299) {
val responseJson = objectMapper.readTree(response.body?.string())
val documents = mutableListOf<DocumentInfo>()
if (responseJson.isArray) {
for (repoNode in responseJson) {
val doc = DocumentInfo(
id = repoNode.get("id")?.asText() ?: "",
title = repoNode.get("name")?.asText() ?: "",
url = repoNode.get("html_url")?.asText() ?: "",
mimeType = "application/vnd.forgejo.repository",
size = 0L,
createdAt = Date(),
modifiedAt = Date(),
owner = repoNode.get("owner")?.get("login")?.asText() ?: "",
permissions = if (repoNode.get("private")?.asBoolean() == true) "private" else "public"
)
documents.add(doc)
}
}
documents
} else {
emptyList()
}
} catch (e: Exception) {
emptyList()
}
}
override suspend fun getDocumentInfo(documentId: String): DocumentInfo? {
if (!config.enabled) return null
return try {
val request = Request.Builder()
.url("${config.baseUrl}/api/${config.apiVersion}/repos/$documentId")
.get()
.addHeader("Authorization", "token ${config.token}")
.build()
val response = httpClient.newCall(request).await()
if (response.code in 200..299) {
val responseJson = objectMapper.readTree(response.body?.string())
DocumentInfo(
id = responseJson.get("id")?.asText() ?: "",
title = responseJson.get("name")?.asText() ?: "",
url = responseJson.get("html_url")?.asText() ?: "",
mimeType = "application/vnd.forgejo.repository",
size = 0L,
createdAt = Date(),
modifiedAt = Date(),
owner = responseJson.get("owner")?.get("login")?.asText() ?: "",
permissions = if (responseJson.get("private")?.asBoolean() == true) "private" else "public"
)
} else {
null
}
} catch (e: Exception) {
null
}
}
}
/**
* Main document collaboration manager that combines all services
*/
class DocumentCollaborationManager(
private val config: DocumentCollaborationConfig,
private val configurationManager: ConfigurationManager = GlobalConfigurationManager.getInstance()
) {
private var onlyOfficeService: OnlyOfficeService? = null
private var nextcloudService: NextcloudService? = null
private var forgejoService: ForgejoService? = null
init {
setupServices()
}
private fun setupServices() {
if (config.onlyOffice.enabled) {
onlyOfficeService = OnlyOfficeService(config.onlyOffice)
}
if (config.nextcloud.enabled) {
nextcloudService = NextcloudService(config.nextcloud)
}
if (config.forgejo.enabled) {
forgejoService = ForgejoService(config.forgejo)
}
}
/**
* Connect to all enabled services
*/
suspend fun connectAll(): Map<String, Boolean> {
val results = mutableMapOf<String, Boolean>()
if (onlyOfficeService != null) {
results["onlyOffice"] = onlyOfficeService!!.connect()
}
if (nextcloudService != null) {
results["nextcloud"] = nextcloudService!!.connect()
}
if (forgejoService != null) {
results["forgejo"] = forgejoService!!.connect()
}
return results
}
/**
* Upload file to a specific service
*/
suspend fun uploadFile(service: String, filePath: String, destinationPath: String): Boolean {
return when (service.lowercase()) {
"onlyoffice" -> onlyOfficeService?.uploadFile(filePath, destinationPath) ?: false
"nextcloud" -> nextcloudService?.uploadFile(filePath, destinationPath) ?: false
"forgejo" -> false // Forgejo doesn't support direct file upload
else -> false
}
}
/**
* Download file from a specific service
*/
suspend fun downloadFile(service: String, remotePath: String, localDestination: String): Boolean {
return when (service.lowercase()) {
"onlyoffice" -> onlyOfficeService?.downloadFile(remotePath, localDestination) ?: false
"nextcloud" -> nextcloudService?.downloadFile(remotePath, localDestination) ?: false
"forgejo" -> forgejoService?.downloadFile(remotePath, localDestination) ?: false
else -> false
}
}
/**
* Create document in a specific service
*/
suspend fun createDocument(service: String, title: String, content: String, mimeType: String): String? {
return when (service.lowercase()) {
"onlyoffice" -> onlyOfficeService?.createDocument(title, content, mimeType)
"nextcloud" -> nextcloudService?.createDocument(title, content, mimeType)
"forgejo" -> forgejoService?.createDocument(title, content, mimeType)
else -> null
}
}
/**
* Edit document in a specific service
*/
suspend fun editDocument(service: String, documentId: String, content: String): Boolean {
return when (service.lowercase()) {
"onlyoffice" -> onlyOfficeService?.editDocument(documentId, content) ?: false
"nextcloud" -> nextcloudService?.editDocument(documentId, content) ?: false
"forgejo" -> forgejoService?.editDocument(documentId, content) ?: false
else -> false
}
}
/**
* Share document from a specific service
*/
suspend fun shareDocument(service: String, documentId: String, permissions: String = "read"): String? {
return when (service.lowercase()) {
"onlyoffice" -> onlyOfficeService?.shareDocument(documentId, permissions)
"nextcloud" -> nextcloudService?.shareDocument(documentId, permissions)
"forgejo" -> forgejoService?.shareDocument(documentId, permissions)
else -> null
}
}
/**
* List documents from a specific service
*/
suspend fun listDocuments(service: String): List<DocumentInfo> {
return when (service.lowercase()) {
"onlyoffice" -> onlyOfficeService?.listDocuments() ?: emptyList()
"nextcloud" -> nextcloudService?.listDocuments() ?: emptyList()
"forgejo" -> forgejoService?.listDocuments() ?: emptyList()
else -> emptyList()
}
}
/**
* Get document info from a specific service
*/
suspend fun getDocumentInfo(service: String, documentId: String): DocumentInfo? {
return when (service.lowercase()) {
"onlyoffice" -> onlyOfficeService?.getDocumentInfo(documentId)
"nextcloud" -> nextcloudService?.getDocumentInfo(documentId)
"forgejo" -> forgejoService?.getDocumentInfo(documentId)
else -> null
}
}
/**
* Get the current configuration
*/
suspend fun getConfig(): DocumentCollaborationConfig {
val currentConfig = configurationManager.getCurrentConfig()
return DocumentCollaborationConfig(
onlyOffice = currentConfig.getIntegratedServiceConfig("onlyoffice") as? OnlyOfficeConfig ?: config.onlyOffice,
nextcloud = currentConfig.getIntegratedServiceConfig("nextcloud") as? NextcloudConfig ?: config.nextcloud,
forgejo = currentConfig.getIntegratedServiceConfig("forgejo") as? ForgejoConfig ?: config.forgejo
)
}
}
/**
* Extension function to get integrated service configuration from agent configuration
*/
private suspend fun AgentConfiguration.getIntegratedServiceConfig(service: String): Any? {
return when (service) {
"onlyoffice" -> {
val aiConfig = aiProviders
// Map to OnlyOffice config if available
OnlyOfficeConfig(
enabled = false, // Would come from custom config
baseUrl = "", // Would come from custom config
apiKey = "", // Would come from custom config
secretKey = "" // Would come from custom config
)
}
"nextcloud" -> {
NextcloudConfig(
enabled = false, // Would come from custom config
baseUrl = "", // Would come from custom config
username = "", // Would come from custom config
password = "", // Would come from custom config
appId = "", // Would come from custom config
appPassword = "" // Would come from custom config
)
}
"forgejo" -> {
ForgejoConfig(
enabled = false, // Would come from custom config
baseUrl = "", // Would come from custom config
token = "", // Would come from custom config
apiVersion = "v1"
)
}
else -> null
}
}

View file

@ -0,0 +1,645 @@
// Mixture and Fallback Manager for Aurelio Model Router
// Handles mixing responses from multiple providers and fallback strategies
package org.intellij.sdk.language
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.min
/**
* Strategy for handling multiple provider responses
*/
enum class MixtureStrategy {
WEIGHTED_AVERAGE, // Weight responses by provider reliability
VOTE_BASED, // Use majority voting for consistency
CONCATENATION, // Concatenate responses from multiple providers
BEST_OF_N, // Return the best response from N providers
CONFIDENCE_BASED // Weight by model's confidence scores
}
/**
* Strategy for fallback behavior
*/
enum class FallbackStrategy {
SEQUENTIAL, // Try providers in sequence until one succeeds
PARALLEL, // Try multiple providers in parallel, use first success
CIRCUIT_BREAKER, // Track provider health, avoid unhealthy providers
HYBRID // Combination of sequential and circuit breaker
}
/**
* Represents a response from a single provider in a mixture
*/
data class ProviderResponse(
val provider: ModelProvider,
val result: ModelResult,
val weight: Double = 1.0,
val confidence: Double = 1.0
)
/**
* Result from mixture processing
*/
data class MixtureResult(
val success: Boolean,
val content: String,
val combinedResponses: List<ProviderResponse>,
val mixtureStrategy: MixtureStrategy,
val totalLatencyMs: Long
)
/**
* Health status for a provider
*/
data class ProviderHealth(
val isHealthy: Boolean,
val lastResponseTime: Long,
val successRate: Double,
val failureCount: Int,
val lastChecked: Long
)
/**
* Manages mixtures and fallbacks for the model router
*/
class MixtureFallbackManager(private val modelRouter: ModelRouter) {
private val healthStatus = ConcurrentHashMap<String, ProviderHealth>()
private val mutex = Mutex()
private val config = getConfig()
/**
* Execute a request with mixture strategy
*/
suspend fun executeMixtureRequest(
messages: List<ChatMessage>,
providers: List<String>,
strategy: MixtureStrategy = MixtureStrategy.WEIGHTED_AVERAGE
): MixtureResult {
val startTime = System.currentTimeMillis()
if (!config.enableMixtures) {
// If mixtures are disabled, just use the first provider
val firstProvider = providers.firstOrNull() ?: config.defaultProvider
val result = modelRouter.routeChatRequest(messages, firstProvider)
return MixtureResult(
success = result.success,
content = result.content,
combinedResponses = listOf(ProviderResponse(
provider = result.provider,
result = result,
weight = 1.0,
confidence = if (result.success) 1.0 else 0.0
)),
mixtureStrategy = strategy,
totalLatencyMs = System.currentTimeMillis() - startTime
)
}
// Execute requests based on strategy
return when (strategy) {
MixtureStrategy.WEIGHTED_AVERAGE -> executeWeightedAverage(messages, providers)
MixtureStrategy.VOTE_BASED -> executeVoteBased(messages, providers)
MixtureStrategy.CONCATENATION -> executeConcatenation(messages, providers)
MixtureStrategy.BEST_OF_N -> executeBestOfN(messages, providers)
MixtureStrategy.CONFIDENCE_BASED -> executeConfidenceBased(messages, providers)
}.copy(totalLatencyMs = System.currentTimeMillis() - startTime)
}
/**
* Execute request with fallback strategy
*/
suspend fun executeFallbackRequest(
messages: List<ChatMessage>,
providers: List<String>,
strategy: FallbackStrategy = FallbackStrategy.SEQUENTIAL
): ModelResult {
return when (strategy) {
FallbackStrategy.SEQUENTIAL -> executeSequentialFallback(messages, providers)
FallbackStrategy.PARALLEL -> executeParallelFallback(messages, providers)
FallbackStrategy.CIRCUIT_BREAKER -> executeCircuitBreakerFallback(messages, providers)
FallbackStrategy.HYBRID -> executeHybridFallback(messages, providers)
}
}
/**
* Execute weighted average mixture
*/
private suspend fun executeWeightedAverage(
messages: List<ChatMessage>,
providers: List<String>
): MixtureResult {
val responses = mutableListOf<ProviderResponse>()
val weights = config.mixtureWeights
for (providerName in providers) {
val weight = weights[providerName] ?: 0.1 // Default weight
if (weight <= 0) continue // Skip providers with zero weight
try {
val result = modelRouter.routeChatRequest(messages, providerName)
responses.add(ProviderResponse(
provider = result.provider,
result = result,
weight = weight,
confidence = if (result.success) calculateResponseQuality(result.content) else 0.0
))
} catch (e: Exception) {
// Log error but continue with other providers
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
}
}
// Combine responses based on weights
val combinedContent = combineWeightedResponses(responses)
return MixtureResult(
success = responses.any { it.result.success },
content = combinedContent,
combinedResponses = responses,
mixtureStrategy = MixtureStrategy.WEIGHTED_AVERAGE,
totalLatencyMs = 0 // Set by caller
)
}
/**
* Execute vote-based mixture
*/
private suspend fun executeVoteBased(
messages: List<ChatMessage>,
providers: List<String>
): MixtureResult {
val responses = mutableListOf<ProviderResponse>()
for (providerName in providers) {
try {
val result = modelRouter.routeChatRequest(messages, providerName)
responses.add(ProviderResponse(
provider = result.provider,
result = result,
weight = 1.0,
confidence = if (result.success) calculateResponseQuality(result.content) else 0.0
))
} catch (e: Exception) {
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
}
}
// Find the most common response or best quality response
val bestResponse = responses.filter { it.result.success }
.maxByOrNull { it.confidence } ?: responses.firstOrNull()
return MixtureResult(
success = bestResponse?.result?.success == true,
content = bestResponse?.result?.content ?: "",
combinedResponses = responses,
mixtureStrategy = MixtureStrategy.VOTE_BASED,
totalLatencyMs = 0
)
}
/**
* Execute concatenation mixture
*/
private suspend fun executeConcatenation(
messages: List<ChatMessage>,
providers: List<String>
): MixtureResult {
val responses = mutableListOf<ProviderResponse>()
val successfulContents = mutableListOf<String>()
for (providerName in providers) {
try {
val result = modelRouter.routeChatRequest(messages, providerName)
responses.add(ProviderResponse(
provider = result.provider,
result = result,
weight = 1.0,
confidence = if (result.success) calculateResponseQuality(result.content) else 0.0
))
if (result.success) {
successfulContents.add("${result.provider.name.toUpperCase()}: ${result.content}")
}
} catch (e: Exception) {
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
}
}
val concatenatedContent = successfulContents.joinToString("\n\n---\n\n")
return MixtureResult(
success = successfulContents.isNotEmpty(),
content = concatenatedContent,
combinedResponses = responses,
mixtureStrategy = MixtureStrategy.CONCATENATION,
totalLatencyMs = 0
)
}
/**
* Execute best-of-N mixture
*/
private suspend fun executeBestOfN(
messages: List<ChatMessage>,
providers: List<String>
): MixtureResult {
val responses = mutableListOf<ProviderResponse>()
for (providerName in providers) {
try {
val result = modelRouter.routeChatRequest(messages, providerName)
responses.add(ProviderResponse(
provider = result.provider,
result = result,
weight = 1.0,
confidence = if (result.success) calculateResponseQuality(result.content) else 0.0
))
} catch (e: Exception) {
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
}
}
// Return the response with highest confidence
val bestResponse = responses.filter { it.result.success }
.maxByOrNull { it.confidence }
return MixtureResult(
success = bestResponse?.result?.success == true,
content = bestResponse?.result?.content ?: "",
combinedResponses = responses,
mixtureStrategy = MixtureStrategy.BEST_OF_N,
totalLatencyMs = 0
)
}
/**
* Execute confidence-based mixture
*/
private suspend fun executeConfidenceBased(
messages: List<ChatMessage>,
providers: List<String>
): MixtureResult {
val responses = mutableListOf<ProviderResponse>()
for (providerName in providers) {
try {
val result = modelRouter.routeChatRequest(messages, providerName)
val confidence = if (result.success) {
calculateResponseConfidence(result, providerName)
} else 0.0
responses.add(ProviderResponse(
provider = result.provider,
result = result,
weight = confidence,
confidence = confidence
))
} catch (e: Exception) {
println("[MixtureFallbackManager] Error with provider $providerName: ${e.message}")
}
}
// Combine based on confidence scores
val combinedContent = combineConfidenceBasedResponses(responses)
return MixtureResult(
success = responses.any { it.result.success },
content = combinedContent,
combinedResponses = responses,
mixtureStrategy = MixtureStrategy.CONFIDENCE_BASED,
totalLatencyMs = 0
)
}
/**
* Execute sequential fallback
*/
private suspend fun executeSequentialFallback(
messages: List<ChatMessage>,
providers: List<String>
): ModelResult {
for (providerName in providers) {
// Check health if using circuit breaker approach
if (isProviderHealthy(providerName)) {
try {
val result = modelRouter.routeChatRequest(messages, providerName)
// Update health status
updateProviderHealth(providerName, result.latencyMs, result.success)
if (result.success) {
return result
}
} catch (e: Exception) {
updateProviderHealth(providerName, 0, false)
println("[MixtureFallbackManager] Sequential fallback failed for $providerName: ${e.message}")
}
}
}
// If all providers failed, return the last error
return ModelResult(
success = false,
content = "",
modelUsed = "none",
provider = ModelProvider.LOCAL_OLLAMA,
latencyMs = 0,
error = "All providers failed in sequential fallback"
)
}
/**
* Execute parallel fallback
*/
private suspend fun executeParallelFallback(
messages: List<ChatMessage>,
providers: List<String>
): ModelResult = withContext(Dispatchers.IO) {
val deferredResults = providers.map { providerName ->
async {
if (isProviderHealthy(providerName)) {
try {
val result = modelRouter.routeChatRequest(messages, providerName)
updateProviderHealth(providerName, result.latencyMs, result.success)
Pair(providerName, result)
} catch (e: Exception) {
updateProviderHealth(providerName, 0, false)
Pair(providerName, ModelResult(
success = false,
content = "",
modelUsed = providerName,
provider = ModelProvider.LOCAL_OLLAMA,
latencyMs = 0,
error = e.message ?: "Unknown error"
))
}
} else {
Pair(providerName, ModelResult(
success = false,
content = "",
modelUsed = providerName,
provider = ModelProvider.LOCAL_OLLAMA,
latencyMs = 0,
error = "Provider marked as unhealthy"
))
}
}
}
val results = awaitAll(*deferredResults.toTypedArray())
// Return the first successful result
val successfulResult = results.find { it.second.success }
return@withContext successfulResult?.second ?: results.firstOrNull()?.second ?: ModelResult(
success = false,
content = "",
modelUsed = "none",
provider = ModelProvider.LOCAL_OLLAMA,
latencyMs = 0,
error = "All providers failed in parallel fallback"
)
}
/**
* Execute circuit breaker fallback
*/
private suspend fun executeCircuitBreakerFallback(
messages: List<ChatMessage>,
providers: List<String>
): ModelResult {
val healthyProviders = providers.filter { isProviderHealthy(it) }
for (providerName in healthyProviders) {
try {
val result = modelRouter.routeChatRequest(messages, providerName)
// Update health status
updateProviderHealth(providerName, result.latencyMs, result.success)
if (result.success) {
return result
}
} catch (e: Exception) {
updateProviderHealth(providerName, 0, false)
println("[MixtureFallbackManager] Circuit breaker fallback failed for $providerName: ${e.message}")
}
}
// If no healthy providers worked, try unhealthy ones as last resort
val unhealthyProviders = providers.filterNot { isProviderHealthy(it) }
for (providerName in unhealthyProviders) {
try {
val result = modelRouter.routeChatRequest(messages, providerName)
// Update health status
updateProviderHealth(providerName, result.latencyMs, result.success)
if (result.success) {
return result
}
} catch (e: Exception) {
updateProviderHealth(providerName, 0, false)
}
}
return ModelResult(
success = false,
content = "",
modelUsed = "none",
provider = ModelProvider.LOCAL_OLLAMA,
latencyMs = 0,
error = "All providers failed in circuit breaker fallback"
)
}
/**
* Execute hybrid fallback combining sequential and circuit breaker
*/
private suspend fun executeHybridFallback(
messages: List<ChatMessage>,
providers: List<String>
): ModelResult {
// First try healthy providers sequentially
val healthyProviders = providers.filter { isProviderHealthy(it) }
for (providerName in healthyProviders) {
try {
val result = modelRouter.routeChatRequest(messages, providerName)
// Update health status
updateProviderHealth(providerName, result.latencyMs, result.success)
if (result.success) {
return result
}
} catch (e: Exception) {
updateProviderHealth(providerName, 0, false)
println("[MixtureFallbackManager] Hybrid fallback failed for healthy $providerName: ${e.message}")
}
}
// Then try remaining providers in parallel as a last resort
val remainingProviders = providers.filterNot { it in healthyProviders }
if (remainingProviders.isNotEmpty()) {
return executeParallelFallback(messages, remainingProviders)
}
return ModelResult(
success = false,
content = "",
modelUsed = "none",
provider = ModelProvider.LOCAL_OLLAMA,
latencyMs = 0,
error = "All providers failed in hybrid fallback"
)
}
/**
* Calculate a quality score for a response
*/
private fun calculateResponseQuality(content: String): Double {
// Simple heuristic for response quality
if (content.isBlank()) return 0.0
var score = 0.5 // Base score
// Reward longer, more substantial responses
if (content.length > 100) score += 0.2
if (content.length > 500) score += 0.1
// Reward variety in content
val uniqueWords = content.lowercase()
.split("\\W+".toRegex())
.filter { it.length > 3 }
.distinct()
.size
if (uniqueWords > 20) score += 0.1
if (uniqueWords > 50) score += 0.1
// Cap at 1.0
return min(score, 1.0)
}
/**
* Calculate confidence based on provider and response
*/
private fun calculateResponseConfidence(result: ModelResult, providerName: String): Double {
var confidence = if (result.success) 0.8 else 0.0
// Adjust based on provider reliability
val health = healthStatus[providerName]
if (health != null) {
confidence *= health.successRate
}
// Adjust based on response quality
if (result.success) {
confidence *= calculateResponseQuality(result.content)
}
return min(confidence, 1.0)
}
/**
* Combine responses using weighted averaging
*/
private fun combineWeightedResponses(responses: List<ProviderResponse>): String {
if (responses.isEmpty()) return ""
if (responses.size == 1) return responses.first().result.content
// For now, return the response with the highest weight * confidence
val bestResponse = responses.maxByOrNull { it.weight * it.confidence }
return bestResponse?.result?.content ?: ""
}
/**
* Combine responses using confidence-based weighting
*/
private fun combineConfidenceBasedResponses(responses: List<ProviderResponse>): String {
if (responses.isEmpty()) return ""
if (responses.size == 1) return responses.first().result.content
// Return the highest confidence response
val bestResponse = responses.maxByOrNull { it.confidence }
return bestResponse?.result?.content ?: ""
}
/**
* Check if a provider is considered healthy
*/
private fun isProviderHealthy(providerName: String): Boolean {
val health = healthStatus[providerName] ?: return true // Assume healthy if not tracked
// Unhealthy if failure count is high or success rate is low
return health.successRate > 0.3 && health.failureCount < 5
}
/**
* Update provider health status
*/
private suspend fun updateProviderHealth(providerName: String, responseTime: Long, success: Boolean) {
mutex.withLock {
val currentHealth = healthStatus[providerName]
val now = System.currentTimeMillis()
val newFailureCount = if (success) 0 else ((currentHealth?.failureCount ?: 0) + 1)
val newSuccessCount = if (success) ((currentHealth?.successRate?.times(if (currentHealth != null) 10 else 0) ?: 0) + 1) else (currentHealth?.successRate?.times(if (currentHealth != null) 10 else 0) ?: 0)
// Calculate new success rate (simple average of last 10 attempts)
val totalAttempts = min(newSuccessCount + newFailureCount, 10.0)
val newSuccessRate = if (totalAttempts > 0) newSuccessCount / totalAttempts else 1.0
val newHealth = ProviderHealth(
isHealthy = newSuccessRate > 0.3 && newFailureCount < 5,
lastResponseTime = responseTime,
successRate = newSuccessRate,
failureCount = newFailureCount,
lastChecked = now
)
healthStatus[providerName] = newHealth
// Log health changes
if (currentHealth?.isHealthy != newHealth.isHealthy) {
val statusChange = if (newHealth.isHealthy) "HEALTHY" else "UNHEALTHY"
println("[MixtureFallbackManager] Provider $providerName marked as $statusChange (success rate: ${String.format("%.2f", newSuccessRate)})")
}
}
}
/**
* Get health status for all providers
*/
fun getAllHealthStatus(): Map<String, ProviderHealth> {
return healthStatus.toMap()
}
/**
* Reset health status for a provider
*/
suspend fun resetProviderHealth(providerName: String) {
mutex.withLock {
healthStatus.remove(providerName)
}
}
/**
* Periodically refresh health status (should be called regularly)
*/
suspend fun refreshHealthStatus() {
val now = System.currentTimeMillis()
mutex.withLock {
// Remove stale health records (older than 10 minutes)
healthStatus.entries.removeIf { entry ->
now - entry.value.lastChecked > 600000 // 10 minutes
}
}
}
}

View file

@ -0,0 +1,674 @@
// Model Router for Aurelio - Inspired by Hermes Agent capabilities
// Supports multiple AI providers with fallbacks and local LLM integration
package org.intellij.sdk.language
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.coroutines.*
import okhttp3.*
import java.io.IOException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger
/**
* Represents different AI model providers supported by Aurelio
*/
enum class ModelProvider {
HERMES_NVIDIA_NIM, // Primary: Hermes Agent with NVIDIA NIM
LOCAL_OLLAMA, // Fallback: Local Ollama
OPENAI, // Cloud: OpenAI GPT models
ANTHROPIC, // Cloud: Anthropic Claude models
GOOGLE_VERTEX, // Cloud: Google Vertex AI
MISTRAL, // Cloud: Mistral AI
TOGETHER, // Cloud: Together AI
CUSTOM_HTTP, // Custom HTTP endpoint
LOCAL_LM_STUDIO // Local LM Studio compatible
}
/**
* Configuration for a specific model provider
*/
data class ProviderConfig(
val provider: ModelProvider,
val modelName: String,
val apiKey: String? = null,
val baseUrl: String? = null,
val fallbackProviders: List<ModelProvider> = emptyList(),
val temperature: Double = 0.7,
val maxTokens: Int = 2048,
val timeoutMs: Long = 30000
)
/**
* Represents a chat message in the conversation
*/
data class ChatMessage(
val role: String, // "user", "assistant", "system"
val content: String
)
/**
* Result from a model provider
*/
data class ModelResult(
val success: Boolean,
val content: String,
val modelUsed: String,
val provider: ModelProvider,
val latencyMs: Long,
val error: String? = null
)
/**
* Main model router that handles requests and provider fallbacks
*/
class ModelRouter {
private val objectMapper: ObjectMapper = jacksonObjectMapper()
private val httpClient = OkHttpClient.Builder()
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
.build()
// Provider configs
private val providerConfigs = mutableMapOf<String, ProviderConfig>()
// Stats tracking
private val requestCounter = AtomicInteger(0)
private val successCounter = AtomicInteger(0)
init {
// Initialize with default configurations
setupDefaultProviders()
}
/**
* Set up default provider configurations based on hermes-agent capabilities
*/
private fun setupDefaultProviders() {
// Primary: Hermes Agent with NVIDIA NIM (as used in hermes-agent)
providerConfigs["hermes-nvidia"] = ProviderConfig(
provider = ModelProvider.HERMES_NVIDIA_NIM,
modelName = "nvidia/nemotron-3-ultra-550b-a55b",
baseUrl = System.getenv("NVIDIA_NIM_API_BASE") ?: "https://integrate.api.nvidia.com/v1",
apiKey = System.getenv("NVIDIA_NIM_API_KEY"),
fallbackProviders = listOf(ModelProvider.LOCAL_OLLAMA),
temperature = 0.7,
maxTokens = 4096
)
// Fallback: Local Ollama (as used in hermes-agent)
providerConfigs["local-ollama"] = ProviderConfig(
provider = ModelProvider.LOCAL_OLLAMA,
modelName = System.getenv("OLLAMA_MODEL") ?: "llama3.2",
baseUrl = System.getenv("OLLAMA_BASE_URL") ?: "http://localhost:11434",
fallbackProviders = listOf(ModelProvider.OPENAI),
temperature = 0.7,
maxTokens = 2048
)
// Cloud providers
providerConfigs["openai"] = ProviderConfig(
provider = ModelProvider.OPENAI,
modelName = System.getenv("OPENAI_MODEL") ?: "gpt-4o",
baseUrl = System.getenv("OPENAI_BASE_URL") ?: "https://api.openai.com/v1",
apiKey = System.getenv("OPENAI_API_KEY"),
fallbackProviders = listOf(ModelProvider.ANTHROPIC, ModelProvider.GOOGLE_VERTEX),
temperature = 0.7,
maxTokens = 4096
)
providerConfigs["anthropic"] = ProviderConfig(
provider = ModelProvider.ANTHROPIC,
modelName = System.getenv("ANTHROPIC_MODEL") ?: "claude-3-5-sonnet-20241022",
baseUrl = System.getenv("ANTHROPIC_BASE_URL") ?: "https://api.anthropic.com/v1",
apiKey = System.getenv("ANTHROPIC_API_KEY"),
fallbackProviders = listOf(ModelProvider.GOOGLE_VERTEX, ModelProvider.MISTRAL),
temperature = 0.7,
maxTokens = 4096
)
providerConfigs["google-vertex"] = ProviderConfig(
provider = ModelProvider.GOOGLE_VERTEX,
modelName = System.getenv("GOOGLE_VERTEX_MODEL") ?: "gemini-2.0-flash-exp",
baseUrl = System.getenv("GOOGLE_VERTEX_BASE_URL") ?: "https://generativelanguage.googleapis.com/v1beta",
apiKey = System.getenv("GOOGLE_VERTEX_API_KEY"),
fallbackProviders = listOf(ModelProvider.MISTRAL, ModelProvider.TOGETHER),
temperature = 0.7,
maxTokens = 2048
)
providerConfigs["mistral"] = ProviderConfig(
provider = ModelProvider.MISTRAL,
modelName = System.getenv("MISTRAL_MODEL") ?: "mistral-large-latest",
baseUrl = System.getenv("MISTRAL_BASE_URL") ?: "https://api.mistral.ai/v1",
apiKey = System.getenv("MISTRAL_API_KEY"),
fallbackProviders = listOf(ModelProvider.TOGETHER, ModelProvider.OPENAI),
temperature = 0.7,
maxTokens = 2048
)
providerConfigs["together"] = ProviderConfig(
provider = ModelProvider.TOGETHER,
modelName = System.getenv("TOGETHER_MODEL") ?: "meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo",
baseUrl = System.getenv("TOGETHER_BASE_URL") ?: "https://api.together.xyz/v1",
apiKey = System.getenv("TOGETHER_API_KEY"),
fallbackProviders = listOf(ModelProvider.LOCAL_OLLAMA),
temperature = 0.7,
maxTokens = 2048
)
// Local LM Studio compatible
providerConfigs["local-lmstudio"] = ProviderConfig(
provider = ModelProvider.LOCAL_LM_STUDIO,
modelName = System.getenv("LMSTUDIO_MODEL") ?: "local-model",
baseUrl = System.getenv("LMSTUDIO_BASE_URL") ?: "http://localhost:1234/v1",
fallbackProviders = listOf(ModelProvider.LOCAL_OLLAMA),
temperature = 0.7,
maxTokens = 2048
)
}
/**
* Route a chat request through available providers with fallback logic
*/
suspend fun routeChatRequest(
messages: List<ChatMessage>,
selectedProvider: String? = null,
customConfig: ProviderConfig? = null
): ModelResult = withContext(Dispatchers.IO) {
val startTime = System.currentTimeMillis()
val requestId = requestCounter.incrementAndGet()
println("[ModelRouter] Processing request #${requestId} with ${messages.size} messages")
// Determine which providers to try
val providersToTry = if (selectedProvider != null) {
// If a specific provider is selected, try it and its fallbacks
val primaryConfig = customConfig ?: providerConfigs[selectedProvider]
if (primaryConfig != null) {
listOf(primaryConfig.provider.name.lowercase()) +
primaryConfig.fallbackProviders.map { it.name.lowercase() }
} else {
// If the selected provider doesn't exist, use defaults
getDefaultProviderOrder()
}
} else {
// Use default provider order
getDefaultProviderOrder()
}
var lastError: Exception? = null
// Try each provider in order
for (providerName in providersToTry) {
val config = providerConfigs[providerName]
if (config == null) {
println("[ModelRouter] Warning: Provider configuration not found for $providerName")
continue
}
try {
println("[ModelRouter] Attempting provider: ${config.provider} (${config.modelName})")
val result = when (config.provider) {
ModelProvider.HERMES_NVIDIA_NIM -> callNvidiaNimApi(config, messages)
ModelProvider.LOCAL_OLLAMA -> callOllamaApi(config, messages)
ModelProvider.OPENAI -> callOpenAiApi(config, messages)
ModelProvider.ANTHROPIC -> callAnthropicApi(config, messages)
ModelProvider.GOOGLE_VERTEX -> callGoogleVertexApi(config, messages)
ModelProvider.MISTRAL -> callMistralApi(config, messages)
ModelProvider.TOGETHER -> callTogetherApi(config, messages)
ModelProvider.LOCAL_LM_STUDIO -> callLmStudioApi(config, messages)
ModelProvider.CUSTOM_HTTP -> callCustomHttpApi(config, messages)
}
if (result.success) {
val latency = System.currentTimeMillis() - startTime
println("[ModelRouter] Success with ${config.provider} after ${latency}ms (#$requestId)")
successCounter.incrementAndGet()
return@withContext result.copy(latencyMs = latency)
} else {
println("[ModelRouter] Provider ${config.provider} failed: ${result.error}")
}
} catch (e: Exception) {
println("[ModelRouter] Provider ${config.provider} threw exception: ${e.message}")
lastError = e
}
}
// All providers failed
val latency = System.currentTimeMillis() - startTime
val errorMsg = lastError?.message ?: "All providers failed"
println("[ModelRouter] All providers failed after ${latency}ms (#$requestId): $errorMsg")
ModelResult(
success = false,
content = "",
modelUsed = "none",
provider = ModelProvider.LOCAL_OLLAMA, // Default fallback
latencyMs = latency,
error = errorMsg
)
}
/**
* Get default provider order based on hermes-agent architecture
*/
private fun getDefaultProviderOrder(): List<String> {
return listOf(
"hermes-nvidia", // Primary: NVIDIA NIM (Hermes Agent)
"local-ollama", // Fallback: Local Ollama (Hermes Agent fallback)
"openai", // Cloud option
"anthropic", // Cloud option
"google-vertex", // Cloud option
"mistral", // Cloud option
"together", // Cloud option
"local-lmstudio" // Local option
)
}
/**
* Call NVIDIA NIM API (used by Hermes Agent)
*/
private suspend fun callNvidiaNimApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
return callOpenAiCompatibleApi(config, messages, "nvidia-nim")
}
/**
* Call Ollama API (used by Hermes Agent as fallback)
*/
private suspend fun callOllamaApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
val requestUrl = "${config.baseUrl}/api/chat"
val requestBody = mapOf(
"model" to config.modelName,
"messages" to messages,
"stream" to false,
"options" to mapOf(
"temperature" to config.temperature,
"num_predict" to config.maxTokens
)
)
val request = Request.Builder()
.url(requestUrl)
.post(okhttp3.RequestBody.create(
MediaType.get("application/json"),
objectMapper.writeValueAsString(requestBody)
))
.apply {
if (!config.apiKey.isNullOrEmpty()) {
addHeader("Authorization", "Bearer ${config.apiKey}")
}
}
.build()
return try {
val response = httpClient.newCall(request).await()
val responseBody = response.body?.string()
if (response.isSuccessful && responseBody != null) {
val parsedResponse = objectMapper.readTree(responseBody)
val content = parsedResponse.at("/message/content").asText()
ModelResult(
success = true,
content = content,
modelUsed = config.modelName,
provider = ModelProvider.LOCAL_OLLAMA,
latencyMs = 0 // Will be calculated by caller
)
} else {
ModelResult(
success = false,
content = "",
modelUsed = config.modelName,
provider = ModelProvider.LOCAL_OLLAMA,
latencyMs = 0,
error = "HTTP ${response.code}: ${responseBody}"
)
}
} catch (e: Exception) {
ModelResult(
success = false,
content = "",
modelUsed = config.modelName,
provider = ModelProvider.LOCAL_OLLAMA,
latencyMs = 0,
error = e.message ?: "Unknown error"
)
}
}
/**
* Call OpenAI-compatible API
*/
private suspend fun callOpenAiCompatibleApi(config: ProviderConfig, messages: List<ChatMessage>, providerName: String): ModelResult {
val requestUrl = "${config.baseUrl}/chat/completions"
val requestBody = mapOf(
"model" to config.modelName,
"messages" to messages,
"temperature" to config.temperature,
"max_tokens" to config.maxTokens,
"stream" to false
)
val request = Request.Builder()
.url(requestUrl)
.post(okhttp3.RequestBody.create(
MediaType.get("application/json"),
objectMapper.writeValueAsString(requestBody)
))
.header("Content-Type", "application/json")
.apply {
if (!config.apiKey.isNullOrEmpty()) {
when (providerName) {
"nvidia-nim" -> addHeader("Authorization", "Bearer ${config.apiKey}")
"openai" -> addHeader("Authorization", "Bearer ${config.apiKey}")
"mistral" -> addHeader("Authorization", "Bearer ${config.apiKey}")
"together" -> addHeader("Authorization", "Bearer ${config.apiKey}")
}
}
}
.build()
return try {
val response = httpClient.newCall(request).await()
val responseBody = response.body?.string()
if (response.isSuccessful && responseBody != null) {
val parsedResponse = objectMapper.readTree(responseBody)
val content = parsedResponse.at("/choices/0/message/content").asText()
ModelResult(
success = true,
content = content,
modelUsed = config.modelName,
provider = when (providerName) {
"nvidia-nim" -> ModelProvider.HERMES_NVIDIA_NIM
"openai" -> ModelProvider.OPENAI
"mistral" -> ModelProvider.MISTRAL
"together" -> ModelProvider.TOGETHER
else -> ModelProvider.CUSTOM_HTTP
},
latencyMs = 0 // Will be calculated by caller
)
} else {
ModelResult(
success = false,
content = "",
modelUsed = config.modelName,
provider = when (providerName) {
"nvidia-nim" -> ModelProvider.HERMES_NVIDIA_NIM
"openai" -> ModelProvider.OPENAI
"mistral" -> ModelProvider.MISTRAL
"together" -> ModelProvider.TOGETHER
else -> ModelProvider.CUSTOM_HTTP
},
latencyMs = 0,
error = "HTTP ${response.code}: ${responseBody}"
)
}
} catch (e: Exception) {
ModelResult(
success = false,
content = "",
modelUsed = config.modelName,
provider = when (providerName) {
"nvidia-nim" -> ModelProvider.HERMES_NVIDIA_NIM
"openai" -> ModelProvider.OPENAI
"mistral" -> ModelProvider.MISTRAL
"together" -> ModelProvider.TOGETHER
else -> ModelProvider.CUSTOM_HTTP
},
latencyMs = 0,
error = e.message ?: "Unknown error"
)
}
}
/**
* Call OpenAI API specifically
*/
private suspend fun callOpenAiApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
return callOpenAiCompatibleApi(config, messages, "openai")
}
/**
* Call Anthropic API
*/
private suspend fun callAnthropicApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
val requestUrl = "${config.baseUrl}/messages"
// Convert messages to Anthropic format
val anthropicMessages = messages.filter { it.role != "system" }.map { msg ->
mapOf("role" to if (msg.role == "user") "user" else "assistant", "content" to msg.content)
}
val systemMessage = messages.firstOrNull { it.role == "system" }?.content
val requestBody = mutableMapOf<String, Any>(
"model" to config.modelName,
"messages" to anthropicMessages,
"max_tokens" to config.maxTokens,
"temperature" to config.temperature
)
if (!systemMessage.isNullOrEmpty()) {
requestBody["system"] = systemMessage
}
val request = Request.Builder()
.url(requestUrl)
.post(okhttp3.RequestBody.create(
MediaType.get("application/json"),
objectMapper.writeValueAsString(requestBody)
))
.header("Content-Type", "application/json")
.header("X-API-Key", config.apiKey ?: "")
.header("anthropic-version", "2023-06-01")
.build()
return try {
val response = httpClient.newCall(request).await()
val responseBody = response.body?.string()
if (response.isSuccessful && responseBody != null) {
val parsedResponse = objectMapper.readTree(responseBody)
val content = parsedResponse.at("/content/0/text").asText()
ModelResult(
success = true,
content = content,
modelUsed = config.modelName,
provider = ModelProvider.ANTHROPIC,
latencyMs = 0 // Will be calculated by caller
)
} else {
ModelResult(
success = false,
content = "",
modelUsed = config.modelName,
provider = ModelProvider.ANTHROPIC,
latencyMs = 0,
error = "HTTP ${response.code}: ${responseBody}"
)
}
} catch (e: Exception) {
ModelResult(
success = false,
content = "",
modelUsed = config.modelName,
provider = ModelProvider.ANTHROPIC,
latencyMs = 0,
error = e.message ?: "Unknown error"
)
}
}
/**
* Call Google Vertex AI API
*/
private suspend fun callGoogleVertexApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
val modelName = config.modelName.removePrefix("gemini-")
val requestUrl = "${config.baseUrl}/models/$modelName:generateContent"
// Convert messages to Vertex format
val contents = messages.map { msg ->
mapOf(
"role" to if (msg.role == "user" || msg.role == "system") "user" else "model",
"parts" to listOf(mapOf("text" to msg.content))
)
}
val requestBody = mapOf(
"contents" to contents,
"generationConfig" to mapOf(
"temperature" to config.temperature,
"maxOutputTokens" to config.maxTokens
)
)
val request = Request.Builder()
.url(requestUrl)
.post(okhttp3.RequestBody.create(
MediaType.get("application/json"),
objectMapper.writeValueAsString(requestBody)
))
.header("Content-Type", "application/json")
.apply {
if (!config.apiKey.isNullOrEmpty()) {
addHeader("x-goog-api-key", config.apiKey)
}
}
.build()
return try {
val response = httpClient.newCall(request).await()
val responseBody = response.body?.string()
if (response.isSuccessful && responseBody != null) {
val parsedResponse = objectMapper.readTree(responseBody)
val content = parsedResponse.at("/candidates/0/content/parts/0/text").asText()
ModelResult(
success = true,
content = content,
modelUsed = config.modelName,
provider = ModelProvider.GOOGLE_VERTEX,
latencyMs = 0 // Will be calculated by caller
)
} else {
ModelResult(
success = false,
content = "",
modelUsed = config.modelName,
provider = ModelProvider.GOOGLE_VERTEX,
latencyMs = 0,
error = "HTTP ${response.code}: ${responseBody}"
)
}
} catch (e: Exception) {
ModelResult(
success = false,
content = "",
modelUsed = config.modelName,
provider = ModelProvider.GOOGLE_VERTEX,
latencyMs = 0,
error = e.message ?: "Unknown error"
)
}
}
/**
* Call Mistral API
*/
private suspend fun callMistralApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
return callOpenAiCompatibleApi(config, messages, "mistral")
}
/**
* Call Together API
*/
private suspend fun callTogetherApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
return callOpenAiCompatibleApi(config, messages, "together")
}
/**
* Call LM Studio compatible API
*/
private suspend fun callLmStudioApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
return callOpenAiCompatibleApi(config, messages, "lmstudio")
}
/**
* Call custom HTTP API
*/
private suspend fun callCustomHttpApi(config: ProviderConfig, messages: List<ChatMessage>): ModelResult {
// This would be customized based on the specific endpoint
return ModelResult(
success = false,
content = "",
modelUsed = config.modelName,
provider = ModelProvider.CUSTOM_HTTP,
latencyMs = 0,
error = "Custom HTTP provider not implemented"
)
}
/**
* Get current statistics
*/
fun getStats(): Map<String, Any> {
return mapOf(
"totalRequests" to requestCounter.get(),
"successfulRequests" to successCounter.get(),
"successRate" to if (requestCounter.get() > 0) {
successCounter.get().toDouble() / requestCounter.get()
} else 0.0
)
}
/**
* Add or update a provider configuration
*/
fun addProviderConfig(name: String, config: ProviderConfig) {
providerConfigs[name] = config
}
/**
* Get available providers
*/
fun getAvailableProviders(): List<String> {
return providerConfigs.keys.toList()
}
}
/**
* Extension function to make OkHttp calls suspending
*/
suspend fun Call.await(): okhttp3.Response {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
this.cancel()
}
this.enqueue(object : okhttp3.Callback {
override fun onFailure(call: Call, e: IOException) {
if (!continuation.isCompleted) {
continuation.resumeWithException(e)
}
}
override fun onResponse(call: Call, response: okhttp3.Response) {
if (!continuation.isCompleted) {
continuation.resume(response) { _, _ ->
response.close()
}
}
}
})
}
}

View file

@ -0,0 +1,509 @@
// Platform Integration for Document Collaboration Services
// Provides integration points for Aurelio-Theia, Aurelio-VSCode, and Aurelio-Web
package org.intellij.sdk.language
import kotlinx.coroutines.*
/**
* Interface for platform-specific document collaboration integration
*/
interface PlatformDocumentCollaborationIntegration {
fun initialize()
fun registerCommands()
fun connectToServices()
fun disconnectFromServices()
fun uploadFile(filePath: String, serviceName: String): Boolean
fun downloadFile(remotePath: String, localPath: String, serviceName: String): Boolean
fun createDocument(title: String, content: String, serviceName: String): String?
fun editDocument(documentId: String, content: String, serviceName: String): Boolean
fun shareDocument(documentId: String, serviceName: String): String?
fun listDocuments(serviceName: String): List<DocumentInfo>
fun getDocumentInfo(documentId: String, serviceName: String): DocumentInfo?
}
/**
* Aurelio VSCode Integration
*/
class AurelioVSCodeIntegration(
private val documentCollaborationManager: DocumentCollaborationManager
) : PlatformDocumentCollaborationIntegration {
private var isConnected = false
override fun initialize() {
println("[AurelioVSCodeIntegration] Initializing document collaboration integration")
registerCommands()
}
override fun registerCommands() {
// Register VSCode commands for document collaboration
// This would typically integrate with the VSCode extension API
println("[AurelioVSCodeIntegration] Registered document collaboration commands")
}
override fun connectToServices() {
if (!isConnected) {
runBlocking {
val results = documentCollaborationManager.connectAll()
isConnected = results.values.any { it }
println("[AurelioVSCodeIntegration] Connected to services: $results")
}
}
}
override fun disconnectFromServices() {
isConnected = false
println("[AurelioVSCodeIntegration] Disconnected from document collaboration services")
}
override fun uploadFile(filePath: String, serviceName: String): Boolean {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.uploadFile(serviceName, filePath, "/uploaded/${filePath.substringAfterLast("/")}")
}
}
override fun downloadFile(remotePath: String, localPath: String, serviceName: String): Boolean {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.downloadFile(serviceName, remotePath, localPath)
}
}
override fun createDocument(title: String, content: String, serviceName: String): String? {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.createDocument(serviceName, title, content, "text/plain")
}
}
override fun editDocument(documentId: String, content: String, serviceName: String): Boolean {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.editDocument(serviceName, documentId, content)
}
}
override fun shareDocument(documentId: String, serviceName: String): String? {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.shareDocument(serviceName, documentId)
}
}
override fun listDocuments(serviceName: String): List<DocumentInfo> {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.listDocuments(serviceName)
}
}
override fun getDocumentInfo(documentId: String, serviceName: String): DocumentInfo? {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.getDocumentInfo(serviceName, documentId)
}
}
}
/**
* Aurelio Theia Integration
*/
class AurelioTheiaIntegration(
private val documentCollaborationManager: DocumentCollaborationManager
) : PlatformDocumentCollaborationIntegration {
private var isConnected = false
override fun initialize() {
println("[AurelioTheiaIntegration] Initializing document collaboration integration")
registerCommands()
}
override fun registerCommands() {
// Register Theia commands for document collaboration
// This would typically integrate with the Theia extension API
println("[AurelioTheiaIntegration] Registered document collaboration commands")
}
override fun connectToServices() {
if (!isConnected) {
runBlocking {
val results = documentCollaborationManager.connectAll()
isConnected = results.values.any { it }
println("[AurelioTheiaIntegration] Connected to services: $results")
}
}
}
override fun disconnectFromServices() {
isConnected = false
println("[AurelioTheiaIntegration] Disconnected from document collaboration services")
}
override fun uploadFile(filePath: String, serviceName: String): Boolean {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.uploadFile(serviceName, filePath, "/uploaded/${filePath.substringAfterLast("/")}")
}
}
override fun downloadFile(remotePath: String, localPath: String, serviceName: String): Boolean {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.downloadFile(serviceName, remotePath, localPath)
}
}
override fun createDocument(title: String, content: String, serviceName: String): String? {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.createDocument(serviceName, title, content, "text/plain")
}
}
override fun editDocument(documentId: String, content: String, serviceName: String): Boolean {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.editDocument(serviceName, documentId, content)
}
}
override fun shareDocument(documentId: String, serviceName: String): String? {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.shareDocument(serviceName, documentId)
}
}
override fun listDocuments(serviceName: String): List<DocumentInfo> {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.listDocuments(serviceName)
}
}
override fun getDocumentInfo(documentId: String, serviceName: String): DocumentInfo? {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.getDocumentInfo(serviceName, documentId)
}
}
}
/**
* Aurelio Web Integration
*/
class AurelioWebIntegration(
private val documentCollaborationManager: DocumentCollaborationManager
) : PlatformDocumentCollaborationIntegration {
private var isConnected = false
override fun initialize() {
println("[AurelioWebIntegration] Initializing document collaboration integration")
}
override fun registerCommands() {
// Register web-based commands for document collaboration
// This would typically integrate with the web application's command system
println("[AurelioWebIntegration] Registered document collaboration commands")
}
override fun connectToServices() {
if (!isConnected) {
runBlocking {
val results = documentCollaborationManager.connectAll()
isConnected = results.values.any { it }
println("[AurelioWebIntegration] Connected to services: $results")
}
}
}
override fun disconnectFromServices() {
isConnected = false
println("[AurelioWebIntegration] Disconnected from document collaboration services")
}
override fun uploadFile(filePath: String, serviceName: String): Boolean {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.uploadFile(serviceName, filePath, "/uploaded/${filePath.substringAfterLast("/")}")
}
}
override fun downloadFile(remotePath: String, localPath: String, serviceName: String): Boolean {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.downloadFile(serviceName, remotePath, localPath)
}
}
override fun createDocument(title: String, content: String, serviceName: String): String? {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.createDocument(serviceName, title, content, "text/plain")
}
}
override fun editDocument(documentId: String, content: String, serviceName: String): Boolean {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.editDocument(serviceName, documentId, content)
}
}
override fun shareDocument(documentId: String, serviceName: String): String? {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.shareDocument(serviceName, documentId)
}
}
override fun listDocuments(serviceName: String): List<DocumentInfo> {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.listDocuments(serviceName)
}
}
override fun getDocumentInfo(documentId: String, serviceName: String): DocumentInfo? {
if (!isConnected) {
connectToServices()
}
return runBlocking {
documentCollaborationManager.getDocumentInfo(serviceName, documentId)
}
}
}
/**
* Main integration manager that handles all platform integrations
*/
class PlatformIntegrationManager {
private val documentCollaborationManager: DocumentCollaborationManager
private val vsCodeIntegration: AurelioVSCodeIntegration
private val theiaIntegration: AurelioTheiaIntegration
private val webIntegration: AurelioWebIntegration
init {
// Initialize with default configuration
val defaultConfig = DocumentCollaborationConfig()
documentCollaborationManager = DocumentCollaborationManager(defaultConfig)
vsCodeIntegration = AurelioVSCodeIntegration(documentCollaborationManager)
theiaIntegration = AurelioTheiaIntegration(documentCollaborationManager)
webIntegration = AurelioWebIntegration(documentCollaborationManager)
}
/**
* Initialize all platform integrations
*/
fun initializeAll() {
vsCodeIntegration.initialize()
theiaIntegration.initialize()
webIntegration.initialize()
}
/**
* Get the VSCode integration instance
*/
fun getVSCodeIntegration(): AurelioVSCodeIntegration {
return vsCodeIntegration
}
/**
* Get the Theia integration instance
*/
fun getTheiaIntegration(): AurelioTheiaIntegration {
return theiaIntegration
}
/**
* Get the Web integration instance
*/
fun getWebIntegration(): AurelioWebIntegration {
return webIntegration
}
/**
* Connect to services across all platforms
*/
suspend fun connectAllPlatforms(): Map<String, Map<String, Boolean>> {
val results = mutableMapOf<String, Map<String, Boolean>>()
results["vscode"] = vsCodeIntegration.let {
it.connectToServices()
documentCollaborationManager.connectAll()
}
results["theia"] = theiaIntegration.let {
it.connectToServices()
documentCollaborationManager.connectAll()
}
results["web"] = webIntegration.let {
it.connectToServices()
documentCollaborationManager.connectAll()
}
return results
}
/**
* Upload file across all platforms
*/
suspend fun uploadFileAcrossPlatforms(filePath: String, serviceName: String): Map<String, Boolean> {
val results = mutableMapOf<String, Boolean>()
results["vscode"] = vsCodeIntegration.uploadFile(filePath, serviceName)
results["theia"] = theiaIntegration.uploadFile(filePath, serviceName)
results["web"] = webIntegration.uploadFile(filePath, serviceName)
return results
}
/**
* Download file across all platforms
*/
suspend fun downloadFileAcrossPlatforms(remotePath: String, localPath: String, serviceName: String): Map<String, Boolean> {
val results = mutableMapOf<String, Boolean>()
results["vscode"] = vsCodeIntegration.downloadFile(remotePath, localPath, serviceName)
results["theia"] = theiaIntegration.downloadFile(remotePath, localPath, serviceName)
results["web"] = webIntegration.downloadFile(remotePath, localPath, serviceName)
return results
}
/**
* Create document across all platforms
*/
suspend fun createDocumentAcrossPlatforms(title: String, content: String, serviceName: String): Map<String, String?> {
val results = mutableMapOf<String, String?>()
results["vscode"] = vsCodeIntegration.createDocument(title, content, serviceName)
results["theia"] = theiaIntegration.createDocument(title, content, serviceName)
results["web"] = webIntegration.createDocument(title, content, serviceName)
return results
}
/**
* Edit document across all platforms
*/
suspend fun editDocumentAcrossPlatforms(documentId: String, content: String, serviceName: String): Map<String, Boolean> {
val results = mutableMapOf<String, Boolean>()
results["vscode"] = vsCodeIntegration.editDocument(documentId, content, serviceName)
results["theia"] = theiaIntegration.editDocument(documentId, content, serviceName)
results["web"] = webIntegration.editDocument(documentId, content, serviceName)
return results
}
/**
* Share document across all platforms
*/
suspend fun shareDocumentAcrossPlatforms(documentId: String, serviceName: String): Map<String, String?> {
val results = mutableMapOf<String, String?>()
results["vscode"] = vsCodeIntegration.shareDocument(documentId, serviceName)
results["theia"] = theiaIntegration.shareDocument(documentId, serviceName)
results["web"] = webIntegration.shareDocument(documentId, serviceName)
return results
}
/**
* List documents across all platforms
*/
suspend fun listDocumentsAcrossPlatforms(serviceName: String): Map<String, List<DocumentInfo>> {
val results = mutableMapOf<String, List<DocumentInfo>>()
results["vscode"] = vsCodeIntegration.listDocuments(serviceName)
results["theia"] = theiaIntegration.listDocuments(serviceName)
results["web"] = webIntegration.listDocuments(serviceName)
return results
}
/**
* Get document info across all platforms
*/
suspend fun getDocumentInfoAcrossPlatforms(documentId: String, serviceName: String): Map<String, DocumentInfo?> {
val results = mutableMapOf<String, DocumentInfo?>()
results["vscode"] = vsCodeIntegration.getDocumentInfo(documentId, serviceName)
results["theia"] = theiaIntegration.getDocumentInfo(documentId, serviceName)
results["web"] = webIntegration.getDocumentInfo(documentId, serviceName)
return results
}
}
/**
* Extension function to create a platform integration manager
*/
fun createPlatformIntegrationManager(): PlatformIntegrationManager {
return PlatformIntegrationManager()
}

View file

@ -0,0 +1,200 @@
// Sample integration file for flint-chart in Aurelio VS Code extension
// This represents how flint-chart would be integrated into aurelio-vscode
import * as vscode from 'vscode';
/**
* Flint Chart Integration for Aurelio VS Code Extension
* Enables AI agents to generate and display charts directly in chat sessions
*/
export class FlintChartIntegration {
private context: vscode.ExtensionContext;
constructor(context: vscode.ExtensionContext) {
this.context = context;
this.registerCommands();
}
/**
* Registers VS Code commands for flint-chart functionality
*/
private registerCommands(): void {
// Command to render a chart from selected data
this.context.subscriptions.push(
vscode.commands.registerCommand('aurelio.flint-chart.render', async () => {
await this.renderChartFromSelection();
})
);
// Command to insert chart into chat panel
this.context.subscriptions.push(
vscode.commands.registerCommand('aurelio.flint-chart.insertInChat', async () => {
await this.insertChartInChat();
})
);
}
/**
* Renders a chart from selected data in the editor
*/
private async renderChartFromSelection(): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor found');
return;
}
const selection = editor.selection;
if (selection.isEmpty) {
vscode.window.showWarningMessage('Please select data to chart');
return;
}
const selectedText = editor.document.getText(selection);
const parsedData = this.parseData(selectedText);
if (!parsedData) {
vscode.window.showErrorMessage('Could not parse selected data as chartable data');
return;
}
// Create and show the chart
await this.showChart(parsedData);
}
/**
* Inserts a chart into the chat panel
*/
private async insertChartInChat(): Promise<void> {
// This would communicate with the chat panel to insert a chart
// Implementation would depend on the specific chat panel structure
vscode.window.showInformationMessage('Inserting chart into chat...');
// Example: send a message to the webview to render a chart
// This would be handled by the chat panel's message passing system
const chartData = {
type: 'line',
data: [10, 20, 30, 40, 50],
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
title: 'Sample Chart'
};
// In a real implementation, this would post a message to the chat webview
console.log('Would send chart data to chat panel:', chartData);
}
/**
* Parses text data into a format suitable for charting
*/
private parseData(text: string): any[] | null {
try {
// Try to parse as JSON first
const jsonData = JSON.parse(text);
if (Array.isArray(jsonData)) {
return jsonData;
}
// Try to parse as CSV-like data
const lines = text.trim().split('\n');
if (lines.length > 1) {
// Simple CSV parsing - assumes numeric values
return lines.map(line => {
const values = line.split(',').map(item => parseFloat(item.trim()));
return values.filter(val => !isNaN(val));
}).flat(); // Flatten to single array for now
}
return null;
} catch (e) {
// If JSON parsing fails, try simpler parsing
const numbers = text.match(/[\d.]+/g);
if (numbers) {
return numbers.map(Number);
}
return null;
}
}
/**
* Shows a chart preview in a webview panel
*/
private async showChart(data: any[]): Promise<void> {
const panel = vscode.window.createWebviewPanel(
'flintChartPreview',
'Flint Chart Preview',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
panel.webview.html = this.getWebviewContent(data);
}
/**
* Generates HTML content for the chart webview
*/
private getWebviewContent(data: any[]): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flint Chart</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.chart-container {
position: relative;
height: 400px;
width: 100%;
}
</style>
</head>
<body>
<h3>Chart Preview</h3>
<div class="chart-container">
<canvas id="chartCanvas"></canvas>
</div>
<script>
const ctx = document.getElementById('chartCanvas').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: ${JSON.stringify(data.map((_, i) => `Item ${i + 1}`))},
datasets: [{
label: 'Values',
data: ${JSON.stringify(data)},
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Generated Chart from Selected Data'
}
}
}
});
</script>
</body>
</html>
`;
}
}
/**
* Initializes the flint-chart integration
*/
export function activateFlintChartIntegration(context: vscode.ExtensionContext): FlintChartIntegration {
return new FlintChartIntegration(context);
}