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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.8.0"
id("org.jetbrains.kotlin.jvm") version "2.1.0"
id("application")
id("jacoco")
`java-library`
Expand Down
6 changes: 3 additions & 3 deletions common-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ repositories {

dependencies {

implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${properties["kotlin_version"]}")

implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:${properties["kotlin_version"]}")

implementation("com.itangcent:commons:${properties["itangcent_intellij_version"]}") {
exclude("com.google.inject")
Expand Down Expand Up @@ -43,7 +43,7 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-params:${properties["junit_version"]}")
testImplementation("org.junit.jupiter:junit-jupiter-api:${properties["junit_version"]}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${properties["junit_version"]}")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:1.8.0")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:${properties["kotlin_version"]}")
}

tasks.getByName<Test>("test") {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugin_name=EasyYapi
plugin_version=2.7.7.212.0
kotlin.code.style=official
kotlin_version=1.8.0
kotlin_version=2.1.0
junit_version=5.9.2
itangcent_intellij_version=1.7.5
7 changes: 5 additions & 2 deletions idea-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ dependencies {
exclude("com.google.guava", "guava")
}

implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:${properties["kotlin_version"]}")

// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.2")
Expand All @@ -81,6 +81,9 @@ dependencies {
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation("com.squareup.okhttp3:okhttp:4.12.0")

// Official OpenAI SDK for Java
implementation("com.openai:openai-java:0.31.0")
implementation("com.openai:openai-java-client-okhttp:0.31.0")

// https://search.maven.org/artifact/org.mockito.kotlin/mockito-kotlin/3.2.0/jar
testImplementation("org.mockito.kotlin:mockito-kotlin:3.2.0")
Expand All @@ -96,7 +99,7 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-api:${properties["junit_version"]}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${properties["junit_version"]}")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.7.1")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:1.8.0")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:${properties["kotlin_version"]}")
}

tasks.getByName<Test>("test") {
Expand Down
2 changes: 2 additions & 0 deletions idea-plugin/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
kotlin.daemon.jvmargs=-Xmx2g
16 changes: 16 additions & 0 deletions idea-plugin/src/main/kotlin/com/itangcent/ai/AIException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.itangcent.ai

/**
* Base exception for all AI-related errors
*/
open class AIException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)

/**
* Exception thrown when there's an issue with the AI service configuration
*/
class AIConfigurationException(message: String) : AIException(message)

/**
* Exception thrown when there's an error in the AI service API response
*/
class AIApiException(message: String, cause: Throwable? = null) : AIException(message, cause)
15 changes: 15 additions & 0 deletions idea-plugin/src/main/kotlin/com/itangcent/ai/AIMessages.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.itangcent.ai

/**
* Contains messages used by AI services
*/
object AIMessages {
/**
* Default system message for general code-related tasks
*/
const val DEFAULT_SYSTEM_MESSAGE =
"You are a helpful programming assistant with expertise in Java, Kotlin, and related technologies. " +
"You can assist with code understanding, debugging, refactoring, optimization, and design. " +
"Analyze the provided code and context carefully, and provide clear, accurate, and practical responses. " +
"When appropriate, suggest improvements while respecting the existing code structure and patterns."
}
54 changes: 54 additions & 0 deletions idea-plugin/src/main/kotlin/com/itangcent/ai/AIProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.itangcent.ai

/**
* Enum representing the supported AI service providers and their models
*/
enum class AIProvider(val displayName: String, val models: List<AIModel>) {
/**
* OpenAI service
*/
OPENAI(
"OpenAI", listOf(
AIModel("gpt-3.5-turbo", "GPT-3.5 Turbo"),
AIModel("gpt-4", "GPT-4"),
AIModel("gpt-4-turbo", "GPT-4 Turbo"),
AIModel("o3-mini", "O3 Mini"),
AIModel("o1", "O1"),
AIModel("o1-mini", "O1 Mini"),
AIModel("gpt-4o", "GPT-4o")
)
),

/**
* DeepSeek service
*/
DEEPSEEK(
"DeepSeek", listOf(
AIModel("deepseek-chat", "DeepSeek-V3"),
AIModel("deepseek-reasoner", "DeepSeek-R1")
)
);

companion object {
/**
* Get AIProvider by its display name (case-insensitive)
*/
fun fromDisplayName(name: String?): AIProvider? {
return values().find { it.displayName.equals(name, ignoreCase = true) }
}

/**
* Get default model for a specific AI provider
*/
fun getDefaultModel(aiProvider: AIProvider): AIModel? {
return aiProvider.models.firstOrNull()
}
}
}

/**
* Data class representing an AI model
* @param id The model identifier used in API requests
* @param displayName The human-readable name for display in UI
*/
data class AIModel(val id: String, val displayName: String)
29 changes: 29 additions & 0 deletions idea-plugin/src/main/kotlin/com/itangcent/ai/AIService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.itangcent.ai

/**
* Interface for AI service operations
*/
interface AIService {
/**
* Sends a prompt to the AI service and returns the response
* @param prompt The user prompt to send to the AI service
* @throws AIException if there's an error with the AI service
*/
fun sendPrompt(prompt: String): String {
return sendPrompt(AIMessages.DEFAULT_SYSTEM_MESSAGE, prompt)
}

/**
* Sends a prompt to the AI service with a specific system message and returns the response
* @param systemMessage The system message that defines the AI's behavior/role
* @param userPrompt The user prompt to send to the AI service
* @throws AIConfigurationException if there's an issue with the AI service configuration
* @throws AIApiException if there's an error in the AI service API response
*/
fun sendPrompt(systemMessage: String, userPrompt: String): String {
// Default implementation delegates to the simpler method
// Implementations should override this for better efficiency
return sendPrompt(userPrompt)
}
}

68 changes: 68 additions & 0 deletions idea-plugin/src/main/kotlin/com/itangcent/ai/AIServiceCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.itangcent.ai

import com.google.inject.Inject
import com.google.inject.Singleton
import com.itangcent.common.logger.Log
import com.itangcent.idea.plugin.api.cache.ProjectCacheRepository
import com.itangcent.idea.sqlite.SqliteDataResourceHelper
import com.itangcent.idea.sqlite.get
import com.itangcent.idea.sqlite.set
import com.itangcent.idea.utils.DigestUtils
import com.itangcent.intellij.context.ActionContext
import java.util.concurrent.TimeUnit

/**
* Cache for AI service responses to avoid duplicate API calls
*/
@Singleton
class AIServiceCache {

companion object : Log() {
private val EXPIRED_TIME = TimeUnit.DAYS.toMillis(30)
}

@Inject
private lateinit var projectCacheRepository: ProjectCacheRepository

@Inject
private lateinit var actionContext: ActionContext

// Use SqliteDataResourceHelper instead of ConcurrentHashMap for persistent storage
private val beanDAO: SqliteDataResourceHelper.ExpiredBeanDAO by lazy {
val sqliteDataResourceHelper = actionContext.instance(SqliteDataResourceHelper::class)
sqliteDataResourceHelper.getExpiredBeanDAO(
projectCacheRepository.getOrCreateFile(".ai.service.cache.db").path, "AI_SERVICE_CACHE"
)
}

/**
* Get a cached response if available
* @param systemMessage The system message
* @param userPrompt The user prompt
* @return The cached response or null if not found
*/
fun getCachedResponse(systemMessage: String, userPrompt: String): String? {
val key = createCacheKey(systemMessage, userPrompt)
return beanDAO.get(key)
}

/**
* Cache a response
* @param systemMessage The system message
* @param userPrompt The user prompt
* @param response The AI response to cache
*/
fun cacheResponse(systemMessage: String, userPrompt: String, response: String) {
LOG.info("cache response: $userPrompt, $response")
val key = createCacheKey(systemMessage, userPrompt)
beanDAO.set(key, response, System.currentTimeMillis() + EXPIRED_TIME)
}

/**
* Create a cache key from the system message and user prompt using MD5 hash
*/
private fun createCacheKey(systemMessage: String, userPrompt: String): String {
val input = "$systemMessage::$userPrompt"
return DigestUtils.md5(input)
}
}
107 changes: 107 additions & 0 deletions idea-plugin/src/main/kotlin/com/itangcent/ai/AIServiceCacheSupport.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.itangcent.ai

import com.google.inject.matcher.Matchers
import com.itangcent.cache.CacheIndicator
import com.itangcent.common.logger.Log
import com.itangcent.common.spi.SetupAble
import com.itangcent.idea.plugin.settings.helper.AISettingsHelper
import com.itangcent.intellij.context.ActionContext
import org.aopalliance.intercept.MethodInterceptor
import org.aopalliance.intercept.MethodInvocation

/**
* SetupAble class that binds the AIServiceCacheInterceptor to AIService implementations
*/
class AIServiceCacheSupport : SetupAble {
/**
* Initializes and binds the interceptor to AIService implementations
*/
override fun init() {
ActionContext.addDefaultInject { builder ->
builder.bindInterceptor(
Matchers.subclassesOf(AIService::class.java),
Matchers.any(),
AIServiceCacheInterceptor
)
}
}
}

/**
* MethodInterceptor that adds caching functionality to AIService implementations
*/
object AIServiceCacheInterceptor : MethodInterceptor, Log() {

private val aiEnableCache: Boolean
get() {
return ActionContext.getContext()
?.instance(AISettingsHelper::class)
?.aiEnableCache == true
}

private val useCache: Boolean
get() {
return ActionContext.getContext()
?.instance(CacheIndicator::class)
?.useCache == true
}

/**
* Intercepts method calls to AIService implementations and adds caching functionality
*/
override fun invoke(invocation: MethodInvocation): Any? {
// Only intercept sendPrompt methods
val methodName = invocation.method.name
if (methodName != "sendPrompt") {
return invocation.proceed()
}

// Check if caching is enabled
if (!aiEnableCache) {
// If caching is disabled, proceed with the original method
return invocation.proceed()
}

// Extract parameters based on method signature
val args = invocation.arguments
val systemMessage: String
val userPrompt: String

when (args.size) {
1 -> {
// Single parameter version: sendPrompt(prompt: String)
systemMessage = ""
userPrompt = args[0] as String
}

2 -> {
// Two parameter version: sendPrompt(systemMessage: String, userPrompt: String)
systemMessage = args[0] as String
userPrompt = args[1] as String
}

else -> {
// Unknown method signature, proceed with original method
return invocation.proceed()
}
}

val aiServiceCache: AIServiceCache = ActionContext.getContext()!!.instance(AIServiceCache::class)

if (useCache) {
// Check if we have a cached response
val cachedResponse = aiServiceCache.getCachedResponse(systemMessage, userPrompt)
if (!cachedResponse.isNullOrEmpty()) {
return cachedResponse
}
}

// No cached response, call the actual service
val response = invocation.proceed() as String

// Cache the response
aiServiceCache.cacheResponse(systemMessage, userPrompt, response)

return response
}
}
Loading
Loading