Skip to content

Conversation

@jianjun159
Copy link
Contributor

…management

AgentScope-Java Version

[The version of AgentScope-Java you are working on, e.g. 1.0.7, check your pom.xml dependency version or run mvn dependency:tree | grep agentscope-parent:pom(only mac/linux)]

version: 1.0.7

Description

[Please describe the background, purpose, changes made, and how to test this PR]

feat(mysql): add MysqlSkillRepository and associated tests for skill management #599

Checklist

Please check the following items before code is ready to be reviewed.

  • Code has been formatted with mvn spotless:apply
  • All tests are passing (mvn test)
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated (e.g. links, examples, etc.)
  • Code is ready for review

@jianjun159 jianjun159 requested a review from a team January 19, 2026 07:53
@gemini-code-assist
Copy link

Summary of Changes

Hello @jianjun159, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to the AgentScope-Java framework by integrating MySQL as a persistent storage solution for agent skills. It provides a robust and secure mechanism for managing skill metadata and associated resources, allowing for scalable and reliable skill management within AgentScope applications. This addition expands the data persistence options available for AgentScope components.

Highlights

  • New Module Introduction: A new Maven module, agentscope-extensions-skill-mysql, has been added to provide MySQL database integration for skill management.
  • MySQL Skill Repository Implementation: The MysqlSkillRepository class implements the AgentSkillRepository interface, enabling AgentSkill objects and their associated resources to be stored, retrieved, updated, and deleted in a MySQL database.
  • Robust Database Operations: The repository supports automatic table creation, full CRUD operations, transaction management for atomic operations, and robust identifier validation to prevent SQL injection vulnerabilities.
  • Flexible Configuration: A builder pattern is provided for flexible configuration of the MysqlSkillRepository, allowing customization of database and table names, and read/write permissions.
  • Comprehensive Unit Testing: Extensive unit tests have been added to cover constructor behavior, CRUD operations, read-only mode, repository information, and critical SQL injection prevention mechanisms.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a MysqlSkillRepository for persisting agent skills in a MySQL database. The implementation is robust, with good practices like using a builder pattern, transaction management, and validation to prevent SQL injection. The accompanying unit tests are comprehensive and well-structured.

I've identified a few areas for improvement, primarily concerning data integrity, performance, and transactional behavior. My key recommendations are:

  • Enforcing data integrity by adding the documented foreign key constraint to the database schema.
  • Optimizing the getAllSkills method to resolve an N+1 query issue.
  • Revisiting the transaction logic in the save method to handle lists of skills more predictably.
  • A minor performance enhancement in resource insertion and a correction in the corresponding unit test.

Overall, this is a solid contribution. Addressing these points will make the repository more performant and resilient.

Comment on lines 304 to 311
String createResourcesTableSql =
"CREATE TABLE IF NOT EXISTS "
+ getFullTableName(resourcesTableName)
+ " (skill_name VARCHAR(255) NOT NULL, resource_path VARCHAR(500) NOT NULL,"
+ " resource_content LONGTEXT NOT NULL, created_at TIMESTAMP DEFAULT"
+ " CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON"
+ " UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (skill_name, resource_path))"
+ " DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Javadoc for the agentscope_skill_resources table (lines 60-68) mentions a foreign key constraint: FOREIGN KEY (skill_name) REFERENCES agentscope_skills(name) ON DELETE CASCADE. However, this CREATE TABLE statement is missing that constraint.

While the code manually deletes resources in deleteSkillInternal, adding the foreign key at the database level is a best practice for ensuring data integrity and preventing orphaned resource records. It also allows the database to enforce the relationship automatically.

Suggested change
String createResourcesTableSql =
"CREATE TABLE IF NOT EXISTS "
+ getFullTableName(resourcesTableName)
+ " (skill_name VARCHAR(255) NOT NULL, resource_path VARCHAR(500) NOT NULL,"
+ " resource_content LONGTEXT NOT NULL, created_at TIMESTAMP DEFAULT"
+ " CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON"
+ " UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (skill_name, resource_path))"
+ " DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
String createResourcesTableSql =
"CREATE TABLE IF NOT EXISTS "
+ getFullTableName(resourcesTableName)
+ " (skill_name VARCHAR(255) NOT NULL, resource_path VARCHAR(500) NOT NULL,"
+ " resource_content LONGTEXT NOT NULL, created_at TIMESTAMP DEFAULT"
+ " CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON"
+ " UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (skill_name, resource_path),"
+ " FOREIGN KEY (skill_name) REFERENCES " + getFullTableName(skillsTableName) + "(name) ON DELETE CASCADE)"
+ " DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";

Comment on lines 480 to 494
public List<AgentSkill> getAllSkills() {
List<String> skillNames = getAllSkillNames();
List<AgentSkill> skills = new ArrayList<>();

for (String name : skillNames) {
try {
skills.add(getSkill(name));
} catch (Exception e) {
logger.warn("Failed to load skill '{}': {}", name, e.getMessage(), e);
// Continue processing other skills
}
}

return skills;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The getAllSkills() method currently suffers from the N+1 query problem. It first fetches all skill names and then iterates through them, executing getSkill() for each name. Each getSkill() call can perform up to two additional queries. This results in 1 + 2*N queries, where N is the number of skills, which is highly inefficient for a large number of skills.

To optimize this, you can fetch all skills in one query and all resources in another, then map them together in memory. This reduces the database round-trips to a constant number (2), regardless of the number of skills.

Comment on lines 519 to 523
if (exists && !force) {
logger.info("Skill already exists and force=false: {}", skillName);
conn.rollback();
return false;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The save method's transaction handling for a list of skills could lead to unexpected behavior. If one skill in the list already exists (and force=false), the entire transaction is rolled back. This means any preceding, valid skills in the list that were already processed are also rolled back.

This 'all-or-nothing' approach might be unintentional. Consider if it would be better to either:

  1. Check for the existence of all skills before starting the transaction.
  2. Use a separate transaction for each skill in the list.
  3. Throw a specific exception indicating which skill caused the failure, rather than returning false.

Comment on lines 604 to 623
int insertedCount = 0;
for (Map.Entry<String, String> entry : resources.entrySet()) {
String path = entry.getKey();
String content = entry.getValue();

validateResourcePath(path);

try (PreparedStatement stmt = conn.prepareStatement(insertSql)) {
stmt.setString(1, skillName);
stmt.setString(2, path);
stmt.setString(3, content);
int affected = stmt.executeUpdate();
if (affected > 0) {
insertedCount++;
logger.debug("Inserted resource '{}' for skill '{}'", path, skillName);
} else {
logger.warn("Failed to insert resource '{}' for skill '{}'", path, skillName);
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the insertResources method, a new PreparedStatement is created inside the for loop for every resource. This is inefficient as it requires the database to parse the same SQL query repeatedly. For better performance, you should prepare the statement once outside the loop and reuse it.

Suggested change
int insertedCount = 0;
for (Map.Entry<String, String> entry : resources.entrySet()) {
String path = entry.getKey();
String content = entry.getValue();
validateResourcePath(path);
try (PreparedStatement stmt = conn.prepareStatement(insertSql)) {
stmt.setString(1, skillName);
stmt.setString(2, path);
stmt.setString(3, content);
int affected = stmt.executeUpdate();
if (affected > 0) {
insertedCount++;
logger.debug("Inserted resource '{}' for skill '{}'", path, skillName);
} else {
logger.warn("Failed to insert resource '{}' for skill '{}'", path, skillName);
}
}
}
int insertedCount = 0;
try (PreparedStatement stmt = conn.prepareStatement(insertSql)) {
for (Map.Entry<String, String> entry : resources.entrySet()) {
String path = entry.getKey();
String content = entry.getValue();
validateResourcePath(path);
stmt.setString(1, skillName);
stmt.setString(2, path);
stmt.setString(3, content);
int affected = stmt.executeUpdate();
if (affected > 0) {
insertedCount++;
logger.debug("Inserted resource '{}' for skill '{}'", path, skillName);
} else {
logger.warn("Failed to insert resource '{}' for skill '{}'", path, skillName);
}
}
}

when(mockStatement.executeUpdate()).thenReturn(1);
when(mockStatement.executeQuery()).thenReturn(mockResultSet);
when(mockResultSet.next()).thenReturn(false); // skill doesn't exist
when(mockStatement.executeBatch()).thenReturn(new int[] {1, 1});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test mocks executeBatch(), but the insertResources method explicitly avoids batching and calls executeUpdate() in a loop. This makes the test assertion misleading as it's not testing the actual implementation path.

The test likely passes due to a broader mock on executeUpdate(). To make the test clearer and more accurate, this unused mock should be removed.

@codecov
Copy link

codecov bot commented Jan 19, 2026

Codecov Report

❌ Patch coverage is 72.50000% with 99 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...e/skill/repository/mysql/MysqlSkillRepository.java 72.50% 88 Missing and 11 partials ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Collaborator

@AlbumenJ AlbumenJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fang-tech PTAL

@fang-tech
Copy link
Contributor

@fang-tech PTAL

👌

AgentSkill loaded = repo.getSkill("data_analysis");
```

#### MySQL 数据库存储
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not provide a detailed introduction to the usage of MySQLRepo here. The documentation only explains the behavior of the interface. Just add the flag indicating that the MySQL storage in the above database has not been implemented.


- 文件系统存储
- 数据库存储 (暂未实现)
- MySQL 数据库存储
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 数据库存储
    • MySQL数据库存储 (agentscope-extensions-skill-mysql-repository)

<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>agentscope-extensions-skill-mysql</artifactId>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change to "agentscope-extensions-skill-mysql-repository"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plz fix this code bot suggestions. If you think this suggestion is unnecessary, close the suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fang-tech Corresponding modifications have been made in accordance with your suggestions

Copy link
Contributor

@fang-tech fang-tech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, tks for your contribution!


- 文件系统存储
- 数据库存储
- MySQL数据库存储 (agentscope-extensions-skill-mysql-repository)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this chunk


#### File System Storage
- File system storage
- database storage
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete this chunk yet


Bind Tools to Skills for on-demand activation. Avoids context pollution from pre-registering all Tools, only passing relevant Tools to LLM when the Skill is actively used.

**Lifecycle of Progressively Disclosed Tools**: Tool lifecycle remains consistent with Skill lifecycle. Once a Skill is activated, Tools remain available throughout the entire session, avoiding the call failures caused by Tool deactivation after each conversation round in the old mechanism.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not change this statement

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update here, do not change this

conn.rollback();
throw e;
} finally {
conn.setAutoCommit(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If conn.setAutoCommit(true) throws SQLException, it will be caught by the outer catch block and wrapped as RuntimeException, potentially leaving the connection in an inconsistent state.

Copy link
Contributor

@fang-tech fang-tech Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finally {
    try {
        conn.setAutoCommit(true);
    } catch (SQLException e) {
        logger.error("Failed to restore autoCommit", e);
        // Don't rethrow - let try-with-resources close the connection
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue exists in: delete() method (line 745) and clearAllSkills() method (line 908)

*
* <pre>
* CREATE TABLE IF NOT EXISTS agentscope_skills (
* name VARCHAR(255) NOT NULL PRIMARY KEY,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add field skill_id as primary key, do not use name

* ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
*
* CREATE TABLE IF NOT EXISTS agentscope_skill_resources (
* skill_name VARCHAR(255) NOT NULL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also

stmt.setString(2, path);
stmt.setString(3, content);

int affected = stmt.executeUpdate();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calling executeUpdate() in each loop iteration is NOT batch processing. This creates N network round-trips for N resources.
Recommended Fix:

for (Map.Entry<String, String> entry : resources.entrySet()) {
            String path = entry.getKey();
            String content = entry.getValue();

            stmt.setString(1, skillName);
            stmt.setString(2, path);
            stmt.setString(3, content);
            
            stmt.addBatch();  // Add to batch instead of executing immediately
        }
        
        // Execute all inserts in one batch
        int[] results = stmt.executeBatch();
        
        // Count successful insertions
        int insertedCount = 0;
        for (int i = 0; i < results.length; i++) {
            if (results[i] > 0 || results[i] == Statement.SUCCESS_NO_INFO) {
                insertedCount++;
            } else if (results[i] == Statement.EXECUTE_FAILED) {
                logger.error("Failed to insert resource at index {}", i);
            }
        }
        
        logger.debug("Batch inserted {} resources for skill '{}'", insertedCount, skillName);

}

// Validate all resource paths first
for (String path : resources.keySet()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation happens inside insertResources(), which is called after the transaction has started. If validation fails, it causes an unnecessary transaction rollback.

Recommended Fix: Move validation to the pre-check section in save() method:

// Pre-check: validate all skill names and resource paths first
for (AgentSkill skill : skills) {
    validateSkillName(skill.getName());
    
    // Validate resource paths before transaction
    Map<String, String> resources = skill.getResources();
    if (resources != null && !resources.isEmpty()) {
        for (String path : resources.keySet()) {
            validateResourcePath(path);
        }
    }
}

* @throws SQLException if deletion fails
*/
private void deleteSkillInternal(Connection conn, String skillName) throws SQLException {
// Delete resources first (if foreign key doesn't have CASCADE)
Copy link
Contributor

@fang-tech fang-tech Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment says "if foreign key doesn't have CASCADE", but the table creation SQL at line 312 does define ON DELETE CASCADE:
Option A - Remove redundant delete
Option B - Keep explicit delete with updated

* @throws IllegalStateException if createIfNotExist is false and
* database/tables do not exist
*/
public MysqlSkillRepository(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep Simple Constructor + Builder
Just keep Simple Constructor:

public MysqlSkillRepository(datasource, createIfNotExist, writeable);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I have completed the corresponding modifications. Could you please review it again

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make Full-parameter construction method private, because you already have a Builder.

* @throws IllegalStateException if createIfNotExist is false and
* database/tables do not exist
*/
public MysqlSkillRepository(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make Full-parameter construction method private, because you already have a Builder.

Skills need to remain available after application restart, or be shared across different environments. Persistence storage supports:

#### File System Storage
#### File System Storag
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not change here

@@ -170,8 +170,6 @@ ReActAgent agent = ReActAgent.builder()

Bind Tools to Skills for on-demand activation. Avoids context pollution from pre-registering all Tools, only passing relevant Tools to LLM when the Skill is actively used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not delete this chunk

+ " underscore. Invalid value: "
+ identifier);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is to keep only the simplest constructor and the Builder, and retain the Builder.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very sorry. I misunderstood your meaning just now. I have made corresponding corrections. Thank you very much for your correction

@fang-tech
Copy link
Contributor

Update the unnecessary modifications in the English document

@jianjun159
Copy link
Contributor Author

Update the unnecessary modifications in the English document

Ok. Now the content of the Chinese document and the English document is consistent

@fang-tech
Copy link
Contributor

@AlbumenJ help to merge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants