mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-17 22:03:39 +00:00
refactor(chat): 重构聊天服务架构,引入Handler模式
主要变更: 1. 移除ruoyi-ai-copilot模块 2. 重构docker配置目录结构,统一迁移至docs/docker/ 3. 聊天服务引入Handler模式: - 新增ChatHandler接口及多种实现 - DefaultChatHandler: 默认聊天处理 - AgentChatHandler: Agent模式处理 - WorkflowChatHandler: 工作流处理 - ResumeChatHandler: 恢复会话处理 - ChatContextBuilder: 上下文构建器 4. 简化AbstractStreamingChatService和ChatServiceFacade代码 5. 优化各Provider实现,统一代码风格 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>ruoyi-ai-copilot</module>
|
||||
<module>ruoyi-monitor-admin</module>
|
||||
<module>ruoyi-snailjob-server</module>
|
||||
</modules>
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>ruoyi-ai-copilot</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>copilot</name>
|
||||
<description>SpringAI - Alibaba - Copilot</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spring-boot.version>4.0.1</spring-boot.version>
|
||||
<spring-ai.version>2.0.0-M2</spring-ai.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starters -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springaicommunity</groupId>
|
||||
<artifactId>spring-ai-agent-utils</artifactId>
|
||||
<version>0.4.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Schema Validation -->
|
||||
<dependency>
|
||||
<groupId>com.networknt</groupId>
|
||||
<artifactId>json-schema-validator</artifactId>
|
||||
<version>1.0.87</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Diff Utils -->
|
||||
<dependency>
|
||||
<groupId>io.github.java-diff-utils</groupId>
|
||||
<artifactId>java-diff-utils</artifactId>
|
||||
<version>4.12</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.example.demo;
|
||||
|
||||
import com.example.demo.config.AppProperties;
|
||||
import com.example.demo.utils.BrowserUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* 主要功能:
|
||||
* 1. 文件读取、写入、编辑
|
||||
* 2. 目录列表和结构查看
|
||||
* 4. 连续性文件操作
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties(AppProperties.class)
|
||||
public class CopilotApplication {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CopilotApplication.class);
|
||||
|
||||
@Autowired
|
||||
private AppProperties appProperties;
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CopilotApplication.class, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用启动完成后的事件监听器
|
||||
* 自动打开浏览器访问应用首页
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationReady() {
|
||||
AppProperties.Browser browserConfig = appProperties.getBrowser();
|
||||
|
||||
if (!browserConfig.isAutoOpen()) {
|
||||
logger.info("Browser auto-open is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取实际的服务器端口
|
||||
String port = environment.getProperty("server.port", "8080");
|
||||
String actualUrl = browserConfig.getUrl().replace("${server.port:8080}", port);
|
||||
|
||||
logger.info("Application started successfully!");
|
||||
logger.info("Preparing to open browser in {} seconds...", browserConfig.getDelaySeconds());
|
||||
|
||||
// 在新线程中延迟打开浏览器,避免阻塞主线程
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(browserConfig.getDelaySeconds() * 1000L);
|
||||
|
||||
if (BrowserUtil.isValidUrl(actualUrl)) {
|
||||
boolean success = BrowserUtil.openBrowser(actualUrl);
|
||||
if (success) {
|
||||
logger.info("✅ Browser opened successfully: {}", actualUrl);
|
||||
System.out.println("🌐 Web interface opened: " + actualUrl);
|
||||
} else {
|
||||
logger.warn("❌ Failed to open browser automatically");
|
||||
System.out.println("⚠️ Please manually open: " + actualUrl);
|
||||
}
|
||||
} else {
|
||||
logger.error("❌ Invalid URL: {}", actualUrl);
|
||||
System.out.println("⚠️ Invalid URL configured: " + actualUrl);
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Browser opening was interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 应用配置属性
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "app")
|
||||
public class AppProperties {
|
||||
|
||||
private Workspace workspace = new Workspace();
|
||||
private Security security = new Security();
|
||||
private Tools tools = new Tools();
|
||||
private Browser browser = new Browser();
|
||||
|
||||
// Getters and Setters
|
||||
public Workspace getWorkspace() {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
public void setWorkspace(Workspace workspace) {
|
||||
this.workspace = workspace;
|
||||
}
|
||||
|
||||
public Security getSecurity() {
|
||||
return security;
|
||||
}
|
||||
|
||||
public void setSecurity(Security security) {
|
||||
this.security = security;
|
||||
}
|
||||
|
||||
public Tools getTools() {
|
||||
return tools;
|
||||
}
|
||||
|
||||
public void setTools(Tools tools) {
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
public Browser getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
public void setBrowser(Browser browser) {
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批模式
|
||||
*/
|
||||
public enum ApprovalMode {
|
||||
DEFAULT, // 默认模式,危险操作需要确认
|
||||
AUTO_EDIT, // 自动编辑模式,文件编辑不需要确认
|
||||
YOLO // 完全自动模式,所有操作都不需要确认
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作空间配置
|
||||
*/
|
||||
public static class Workspace {
|
||||
// 使用 Paths.get() 和 File.separator 实现跨平台兼容
|
||||
private String rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString();
|
||||
private long maxFileSize = 10485760L; // 10MB
|
||||
private List<String> allowedExtensions = List.of(
|
||||
".txt", ".md", ".java", ".js", ".ts", ".json", ".xml",
|
||||
".yml", ".yaml", ".properties", ".html", ".css", ".sql"
|
||||
);
|
||||
|
||||
// Getters and Setters
|
||||
public String getRootDirectory() {
|
||||
return rootDirectory;
|
||||
}
|
||||
|
||||
public void setRootDirectory(String rootDirectory) {
|
||||
// 确保设置的路径也是跨平台兼容的
|
||||
this.rootDirectory = Paths.get(rootDirectory).toString();
|
||||
}
|
||||
|
||||
public long getMaxFileSize() {
|
||||
return maxFileSize;
|
||||
}
|
||||
|
||||
public void setMaxFileSize(long maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
public List<String> getAllowedExtensions() {
|
||||
return allowedExtensions;
|
||||
}
|
||||
|
||||
public void setAllowedExtensions(List<String> allowedExtensions) {
|
||||
this.allowedExtensions = allowedExtensions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全配置
|
||||
*/
|
||||
public static class Security {
|
||||
private ApprovalMode approvalMode = ApprovalMode.DEFAULT;
|
||||
private List<String> dangerousCommands = List.of("rm", "del", "format", "fdisk", "mkfs");
|
||||
|
||||
// Getters and Setters
|
||||
public ApprovalMode getApprovalMode() {
|
||||
return approvalMode;
|
||||
}
|
||||
|
||||
public void setApprovalMode(ApprovalMode approvalMode) {
|
||||
this.approvalMode = approvalMode;
|
||||
}
|
||||
|
||||
public List<String> getDangerousCommands() {
|
||||
return dangerousCommands;
|
||||
}
|
||||
|
||||
public void setDangerousCommands(List<String> dangerousCommands) {
|
||||
this.dangerousCommands = dangerousCommands;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具配置
|
||||
*/
|
||||
public static class Tools {
|
||||
private ToolConfig readFile = new ToolConfig(true);
|
||||
private ToolConfig writeFile = new ToolConfig(true);
|
||||
private ToolConfig editFile = new ToolConfig(true);
|
||||
private ToolConfig listDirectory = new ToolConfig(true);
|
||||
private ToolConfig shell = new ToolConfig(true);
|
||||
|
||||
// Getters and Setters
|
||||
public ToolConfig getReadFile() {
|
||||
return readFile;
|
||||
}
|
||||
|
||||
public void setReadFile(ToolConfig readFile) {
|
||||
this.readFile = readFile;
|
||||
}
|
||||
|
||||
public ToolConfig getWriteFile() {
|
||||
return writeFile;
|
||||
}
|
||||
|
||||
public void setWriteFile(ToolConfig writeFile) {
|
||||
this.writeFile = writeFile;
|
||||
}
|
||||
|
||||
public ToolConfig getEditFile() {
|
||||
return editFile;
|
||||
}
|
||||
|
||||
public void setEditFile(ToolConfig editFile) {
|
||||
this.editFile = editFile;
|
||||
}
|
||||
|
||||
public ToolConfig getListDirectory() {
|
||||
return listDirectory;
|
||||
}
|
||||
|
||||
public void setListDirectory(ToolConfig listDirectory) {
|
||||
this.listDirectory = listDirectory;
|
||||
}
|
||||
|
||||
public ToolConfig getShell() {
|
||||
return shell;
|
||||
}
|
||||
|
||||
public void setShell(ToolConfig shell) {
|
||||
this.shell = shell;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具配置
|
||||
*/
|
||||
public static class ToolConfig {
|
||||
private boolean enabled;
|
||||
|
||||
public ToolConfig() {
|
||||
}
|
||||
|
||||
public ToolConfig(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器配置
|
||||
*/
|
||||
public static class Browser {
|
||||
private boolean autoOpen = true;
|
||||
private String url = "http://localhost:8080";
|
||||
private int delaySeconds = 2;
|
||||
|
||||
// Getters and Setters
|
||||
public boolean isAutoOpen() {
|
||||
return autoOpen;
|
||||
}
|
||||
|
||||
public void setAutoOpen(boolean autoOpen) {
|
||||
this.autoOpen = autoOpen;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public int getDelaySeconds() {
|
||||
return delaySeconds;
|
||||
}
|
||||
|
||||
public void setDelaySeconds(int delaySeconds) {
|
||||
this.delaySeconds = delaySeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
/**
|
||||
* 处理超时异常
|
||||
*/
|
||||
@ExceptionHandler({TimeoutException.class, AsyncRequestTimeoutException.class})
|
||||
public ResponseEntity<ErrorResponse> handleTimeoutException(Exception e, WebRequest request) {
|
||||
logger.error("Request timeout occurred", e);
|
||||
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"TIMEOUT_ERROR",
|
||||
"Request timed out. The operation took too long to complete.",
|
||||
"Please try again with a simpler request or check your network connection."
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理AI相关异常
|
||||
*/
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e, WebRequest request) {
|
||||
logger.error("Runtime exception occurred", e);
|
||||
|
||||
// 检查是否是AI调用相关的异常
|
||||
String message = e.getMessage();
|
||||
if (message != null && (message.contains("tool") || message.contains("function") || message.contains("AI"))) {
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"AI_TOOL_ERROR",
|
||||
"An error occurred during AI tool execution: " + message,
|
||||
"The AI encountered an issue while processing your request. Please try rephrasing your request or try again."
|
||||
);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
||||
}
|
||||
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"RUNTIME_ERROR",
|
||||
"An unexpected error occurred: " + message,
|
||||
"Please try again. If the problem persists, contact support."
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理所有其他异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGenericException(Exception e, WebRequest request) {
|
||||
logger.error("Unexpected exception occurred", e);
|
||||
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"INTERNAL_ERROR",
|
||||
"An internal server error occurred",
|
||||
"Something went wrong on our end. Please try again later."
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应类
|
||||
*/
|
||||
public static class ErrorResponse {
|
||||
private String errorCode;
|
||||
private String message;
|
||||
private String suggestion;
|
||||
private long timestamp;
|
||||
|
||||
public ErrorResponse(String errorCode, String message, String suggestion) {
|
||||
this.errorCode = errorCode;
|
||||
this.message = message;
|
||||
this.suggestion = suggestion;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getSuggestion() {
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
public void setSuggestion(String suggestion) {
|
||||
this.suggestion = suggestion;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 日志配置类
|
||||
* 确保日志目录存在并记录应用启动信息
|
||||
*/
|
||||
@Configuration
|
||||
public class LoggingConfiguration {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LoggingConfiguration.class);
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationReady() {
|
||||
// 确保日志目录存在
|
||||
File logsDir = new File("logs");
|
||||
if (!logsDir.exists()) {
|
||||
boolean created = logsDir.mkdirs();
|
||||
if (created) {
|
||||
logger.info("📁 创建日志目录: {}", logsDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
// 记录应用启动信息
|
||||
String startTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
logger.info("🎉 ========================================");
|
||||
logger.info("🚀 (♥◠‿◠)ノ゙ AI Copilot启动成功 ლ(´ڡ`ლ)゙");
|
||||
logger.info("🕐 启动时间: {}", startTime);
|
||||
logger.info("📝 日志级别: DEBUG (工具调用详细日志已启用)");
|
||||
logger.info("📁 日志文件: logs/copilot-file-ops.log");
|
||||
logger.info("🔧 支持的工具:");
|
||||
logger.info(" 📖 read_file - 读取文件内容");
|
||||
logger.info(" ✏️ write_file - 写入文件内容");
|
||||
logger.info(" 📝 edit_file - 编辑文件内容");
|
||||
logger.info(" 🔍 analyze_project - 分析项目结构");
|
||||
logger.info(" 🏗️ scaffold_project - 创建项目脚手架");
|
||||
logger.info(" 🧠 smart_edit - 智能编辑项目");
|
||||
logger.info("🎉 ========================================");
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.springaicommunity.agent.tools.FileSystemTools;
|
||||
import org.springaicommunity.agent.tools.ShellTools;
|
||||
import org.springaicommunity.agent.tools.SkillsTool;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Spring AI 配置 - 使用Spring AI 1.0.0规范
|
||||
*/
|
||||
@Configuration
|
||||
public class SpringAIConfiguration {
|
||||
|
||||
@Value("${agent.skills.dirs:Unknown}") List<Resource> agentSkillsDirs;
|
||||
|
||||
@Bean
|
||||
public ChatClient chatClient(ChatModel chatModel, AppProperties appProperties) {
|
||||
// 动态获取工作目录路径
|
||||
ChatClient.Builder chatClientBuilder = ChatClient.builder(chatModel);
|
||||
|
||||
return chatClientBuilder
|
||||
.defaultSystem("Always use the available skills to assist the user in their requests.")
|
||||
// Skills tool callbacks
|
||||
.defaultToolCallbacks(SkillsTool.builder().addSkillsResources(agentSkillsDirs).build())
|
||||
// Built-in tools
|
||||
.defaultTools(
|
||||
// FileSystemTools.builder().build(),
|
||||
ShellTools.builder().build()
|
||||
)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
/**
|
||||
* 任务上下文持有者
|
||||
* 使用ThreadLocal存储当前任务ID,供AOP切面使用
|
||||
*/
|
||||
public class TaskContextHolder {
|
||||
|
||||
private static final ThreadLocal<String> taskIdHolder = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 获取当前任务ID
|
||||
*/
|
||||
public static String getCurrentTaskId() {
|
||||
return taskIdHolder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前任务ID
|
||||
*/
|
||||
public static void setCurrentTaskId(String taskId) {
|
||||
taskIdHolder.set(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前任务ID
|
||||
*/
|
||||
public static void clearCurrentTaskId() {
|
||||
taskIdHolder.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有当前任务ID
|
||||
*/
|
||||
public static boolean hasCurrentTaskId() {
|
||||
return taskIdHolder.get() != null;
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.dto.ChatRequestDto;
|
||||
import com.example.demo.service.ContinuousConversationService;
|
||||
import com.example.demo.service.ToolExecutionLogger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 聊天控制器
|
||||
* 处理与AI的对话和工具调用
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class ChatController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ChatController.class);
|
||||
|
||||
private final ChatClient chatClient;
|
||||
private final ContinuousConversationService continuousConversationService;
|
||||
private final ToolExecutionLogger executionLogger;
|
||||
|
||||
// 简单的会话存储(生产环境应该使用数据库或Redis)
|
||||
private final List<Message> conversationHistory = new ArrayList<>();
|
||||
|
||||
public ChatController(ChatClient chatClient, ContinuousConversationService continuousConversationService, ToolExecutionLogger executionLogger) {
|
||||
this.chatClient = chatClient;
|
||||
this.continuousConversationService = continuousConversationService;
|
||||
this.executionLogger = executionLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式聊天 - 直接返回流式数据
|
||||
*/
|
||||
@PostMapping(value = "/message", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<String> streamMessage(@RequestBody ChatRequestDto request) {
|
||||
logger.info("📨 开始流式聊天: {}", request.getMessage());
|
||||
|
||||
return Flux.create(sink -> {
|
||||
try {
|
||||
UserMessage userMessage = new UserMessage(request.getMessage());
|
||||
conversationHistory.add(userMessage);
|
||||
|
||||
// 使用Spring AI的流式API
|
||||
Flux<String> contentStream = chatClient.prompt()
|
||||
.messages(conversationHistory)
|
||||
.stream()
|
||||
.content();
|
||||
|
||||
// 订阅流式内容并转发给前端
|
||||
contentStream
|
||||
.doOnNext(content -> {
|
||||
logger.debug("📨 流式内容片段: {}", content);
|
||||
// 发送内容片段(SSE格式会自动添加 "data: " 前缀)
|
||||
sink.next(content);
|
||||
})
|
||||
.doOnComplete(() -> {
|
||||
logger.info("✅ 流式聊天完成");
|
||||
// 发送完成标记
|
||||
sink.next("[DONE]");
|
||||
sink.complete();
|
||||
})
|
||||
.doOnError(error -> {
|
||||
logger.error("❌ 流式聊天错误: {}", error.getMessage());
|
||||
sink.error(error);
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("❌ 流式聊天启动失败: {}", e.getMessage());
|
||||
sink.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清除对话历史
|
||||
*/
|
||||
@PostMapping("/clear")
|
||||
public Mono<Map<String, String>> clearHistory() {
|
||||
conversationHistory.clear();
|
||||
logger.info("Conversation history cleared");
|
||||
return Mono.just(Map.of("status", "success", "message", "Conversation history cleared"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话历史
|
||||
*/
|
||||
@GetMapping("/history")
|
||||
public Mono<List<MessageDto>> getHistory() {
|
||||
List<MessageDto> history = conversationHistory.stream()
|
||||
.map(message -> {
|
||||
MessageDto dto = new MessageDto();
|
||||
dto.setContent(message.getText());
|
||||
dto.setRole(message instanceof UserMessage ? "user" : "assistant");
|
||||
return dto;
|
||||
})
|
||||
.toList();
|
||||
|
||||
return Mono.just(history);
|
||||
}
|
||||
|
||||
// 注意:Spring AI 1.0.0 使用不同的函数调用方式
|
||||
// 函数需要在配置中注册,而不是在运行时动态创建
|
||||
|
||||
public static class ChatResponseDto {
|
||||
private String taskId;
|
||||
private String message;
|
||||
private boolean success;
|
||||
private boolean asyncTask;
|
||||
private boolean streamResponse; // 新增:标识是否为流式响应
|
||||
private int totalTurns;
|
||||
private boolean reachedMaxTurns;
|
||||
private String stopReason;
|
||||
private long totalDurationMs;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(boolean success) {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public boolean isAsyncTask() {
|
||||
return asyncTask;
|
||||
}
|
||||
|
||||
public void setAsyncTask(boolean asyncTask) {
|
||||
this.asyncTask = asyncTask;
|
||||
}
|
||||
|
||||
public boolean isStreamResponse() {
|
||||
return streamResponse;
|
||||
}
|
||||
|
||||
public void setStreamResponse(boolean streamResponse) {
|
||||
this.streamResponse = streamResponse;
|
||||
}
|
||||
|
||||
public int getTotalTurns() {
|
||||
return totalTurns;
|
||||
}
|
||||
|
||||
public void setTotalTurns(int totalTurns) {
|
||||
this.totalTurns = totalTurns;
|
||||
}
|
||||
|
||||
public boolean isReachedMaxTurns() {
|
||||
return reachedMaxTurns;
|
||||
}
|
||||
|
||||
public void setReachedMaxTurns(boolean reachedMaxTurns) {
|
||||
this.reachedMaxTurns = reachedMaxTurns;
|
||||
}
|
||||
|
||||
public String getStopReason() {
|
||||
return stopReason;
|
||||
}
|
||||
|
||||
public void setStopReason(String stopReason) {
|
||||
this.stopReason = stopReason;
|
||||
}
|
||||
|
||||
public long getTotalDurationMs() {
|
||||
return totalDurationMs;
|
||||
}
|
||||
|
||||
public void setTotalDurationMs(long totalDurationMs) {
|
||||
this.totalDurationMs = totalDurationMs;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MessageDto {
|
||||
private String content;
|
||||
private String role;
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.service.LogStreamService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* SSE日志流控制器
|
||||
* 提供SSE连接端点
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/logs")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class LogStreamController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LogStreamController.class);
|
||||
|
||||
@Autowired
|
||||
private LogStreamService logStreamService;
|
||||
|
||||
/**
|
||||
* 建立SSE连接
|
||||
* 前端通过此端点建立实时日志推送连接
|
||||
*/
|
||||
@GetMapping(value = "/stream/{taskId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter streamLogs(@PathVariable("taskId") String taskId) {
|
||||
logger.info("🔗 收到SSE连接请求: taskId={}", taskId);
|
||||
|
||||
try {
|
||||
SseEmitter emitter = logStreamService.createConnection(taskId);
|
||||
logger.info("✅ SSE连接建立成功: taskId={}", taskId);
|
||||
return emitter;
|
||||
} catch (Exception e) {
|
||||
logger.error("❌ SSE连接建立失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
throw new RuntimeException("Failed to create SSE connection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭SSE连接
|
||||
*/
|
||||
@PostMapping("/close/{taskId}")
|
||||
public void closeConnection(@PathVariable("taskId") String taskId) {
|
||||
logger.info("🔚 收到关闭SSE连接请求: taskId={}", taskId);
|
||||
logStreamService.closeConnection(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
@GetMapping("/status")
|
||||
public ConnectionStatus getConnectionStatus() {
|
||||
int activeConnections = logStreamService.getActiveConnectionCount();
|
||||
logger.debug("📊 当前活跃SSE连接数: {}", activeConnections);
|
||||
|
||||
ConnectionStatus status = new ConnectionStatus();
|
||||
status.setActiveConnections(activeConnections);
|
||||
status.setStatus("OK");
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态DTO
|
||||
*/
|
||||
public static class ConnectionStatus {
|
||||
private int activeConnections;
|
||||
private String status;
|
||||
|
||||
public int getActiveConnections() {
|
||||
return activeConnections;
|
||||
}
|
||||
|
||||
public void setActiveConnections(int activeConnections) {
|
||||
this.activeConnections = activeConnections;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.model.TaskStatus;
|
||||
import com.example.demo.service.ContinuousConversationService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/task")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class TaskStatusController {
|
||||
|
||||
private final ContinuousConversationService conversationService;
|
||||
|
||||
public TaskStatusController(ContinuousConversationService conversationService) {
|
||||
this.conversationService = conversationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务状态
|
||||
*/
|
||||
@GetMapping("/status/{taskId}")
|
||||
public Mono<TaskStatusDto> getTaskStatus(@PathVariable("taskId") String taskId) {
|
||||
return Mono.fromCallable(() -> {
|
||||
TaskStatus status = conversationService.getTaskStatus(taskId);
|
||||
if (status == null) {
|
||||
throw new RuntimeException("Task not found: " + taskId);
|
||||
}
|
||||
|
||||
TaskStatusDto dto = new TaskStatusDto();
|
||||
dto.setTaskId(status.getTaskId());
|
||||
dto.setStatus(status.getStatus());
|
||||
dto.setCurrentAction(status.getCurrentAction());
|
||||
dto.setSummary(status.getSummary());
|
||||
dto.setCurrentTurn(status.getCurrentTurn());
|
||||
dto.setTotalEstimatedTurns(status.getTotalEstimatedTurns());
|
||||
dto.setProgressPercentage(status.getProgressPercentage());
|
||||
dto.setElapsedTime(status.getElapsedTime());
|
||||
dto.setErrorMessage(status.getErrorMessage());
|
||||
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话结果
|
||||
*/
|
||||
@GetMapping("/result/{taskId}")
|
||||
public Mono<ConversationResultDto> getConversationResult(@PathVariable("taskId") String taskId) {
|
||||
return Mono.fromCallable(() -> {
|
||||
ContinuousConversationService.ConversationResult result = conversationService.getConversationResult(taskId);
|
||||
if (result == null) {
|
||||
throw new RuntimeException("Conversation result not found: " + taskId);
|
||||
}
|
||||
|
||||
ConversationResultDto dto = new ConversationResultDto();
|
||||
dto.setTaskId(taskId);
|
||||
dto.setFullResponse(result.getFullResponse());
|
||||
dto.setTurnResponses(result.getTurnResponses());
|
||||
dto.setTotalTurns(result.getTotalTurns());
|
||||
dto.setReachedMaxTurns(result.isReachedMaxTurns());
|
||||
dto.setStopReason(result.getStopReason());
|
||||
dto.setTotalDurationMs(result.getTotalDurationMs());
|
||||
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
// DTO类
|
||||
public static class TaskStatusDto {
|
||||
private String taskId;
|
||||
private String status;
|
||||
private String currentAction;
|
||||
private String summary;
|
||||
private int currentTurn;
|
||||
private int totalEstimatedTurns;
|
||||
private double progressPercentage;
|
||||
private long elapsedTime;
|
||||
private String errorMessage;
|
||||
|
||||
// Getters and Setters
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getCurrentAction() {
|
||||
return currentAction;
|
||||
}
|
||||
|
||||
public void setCurrentAction(String currentAction) {
|
||||
this.currentAction = currentAction;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public int getCurrentTurn() {
|
||||
return currentTurn;
|
||||
}
|
||||
|
||||
public void setCurrentTurn(int currentTurn) {
|
||||
this.currentTurn = currentTurn;
|
||||
}
|
||||
|
||||
public int getTotalEstimatedTurns() {
|
||||
return totalEstimatedTurns;
|
||||
}
|
||||
|
||||
public void setTotalEstimatedTurns(int totalEstimatedTurns) {
|
||||
this.totalEstimatedTurns = totalEstimatedTurns;
|
||||
}
|
||||
|
||||
public double getProgressPercentage() {
|
||||
return progressPercentage;
|
||||
}
|
||||
|
||||
public void setProgressPercentage(double progressPercentage) {
|
||||
this.progressPercentage = progressPercentage;
|
||||
}
|
||||
|
||||
public long getElapsedTime() {
|
||||
return elapsedTime;
|
||||
}
|
||||
|
||||
public void setElapsedTime(long elapsedTime) {
|
||||
this.elapsedTime = elapsedTime;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// 对话结果DTO类
|
||||
public static class ConversationResultDto {
|
||||
private String taskId;
|
||||
private String fullResponse;
|
||||
private java.util.List<String> turnResponses;
|
||||
private int totalTurns;
|
||||
private boolean reachedMaxTurns;
|
||||
private String stopReason;
|
||||
private long totalDurationMs;
|
||||
|
||||
// Getters and Setters
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getFullResponse() {
|
||||
return fullResponse;
|
||||
}
|
||||
|
||||
public void setFullResponse(String fullResponse) {
|
||||
this.fullResponse = fullResponse;
|
||||
}
|
||||
|
||||
public java.util.List<String> getTurnResponses() {
|
||||
return turnResponses;
|
||||
}
|
||||
|
||||
public void setTurnResponses(java.util.List<String> turnResponses) {
|
||||
this.turnResponses = turnResponses;
|
||||
}
|
||||
|
||||
public int getTotalTurns() {
|
||||
return totalTurns;
|
||||
}
|
||||
|
||||
public void setTotalTurns(int totalTurns) {
|
||||
this.totalTurns = totalTurns;
|
||||
}
|
||||
|
||||
public boolean isReachedMaxTurns() {
|
||||
return reachedMaxTurns;
|
||||
}
|
||||
|
||||
public void setReachedMaxTurns(boolean reachedMaxTurns) {
|
||||
this.reachedMaxTurns = reachedMaxTurns;
|
||||
}
|
||||
|
||||
public String getStopReason() {
|
||||
return stopReason;
|
||||
}
|
||||
|
||||
public void setStopReason(String stopReason) {
|
||||
this.stopReason = stopReason;
|
||||
}
|
||||
|
||||
public long getTotalDurationMs() {
|
||||
return totalDurationMs;
|
||||
}
|
||||
|
||||
public void setTotalDurationMs(long totalDurationMs) {
|
||||
this.totalDurationMs = totalDurationMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* Web页面控制器
|
||||
*/
|
||||
@Controller
|
||||
public class WebController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String index() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理favicon.ico请求,避免404错误
|
||||
*/
|
||||
@GetMapping("/favicon.ico")
|
||||
public ResponseEntity<Void> favicon() {
|
||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
/**
|
||||
* 聊天请求数据传输对象
|
||||
*/
|
||||
public class ChatRequestDto {
|
||||
private String message;
|
||||
private String sessionId; // 可选:用于会话管理
|
||||
|
||||
public ChatRequestDto() {
|
||||
}
|
||||
|
||||
public ChatRequestDto(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ChatRequestDto(String message, String sessionId) {
|
||||
this.message = message;
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChatRequestDto{" +
|
||||
"message='" + message + '\'' +
|
||||
", sessionId='" + sessionId + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Project context information
|
||||
* Contains complete project analysis results for AI understanding
|
||||
*/
|
||||
public class ProjectContext {
|
||||
private Path projectRoot;
|
||||
private ProjectType projectType;
|
||||
private ProjectStructure projectStructure;
|
||||
private List<DependencyInfo> dependencies;
|
||||
private List<ConfigFile> configFiles;
|
||||
private CodeStatistics codeStatistics;
|
||||
private Map<String, Object> metadata;
|
||||
private String contextSummary;
|
||||
|
||||
public ProjectContext() {
|
||||
this.dependencies = new ArrayList<>();
|
||||
this.configFiles = new ArrayList<>();
|
||||
this.metadata = new HashMap<>();
|
||||
}
|
||||
|
||||
public ProjectContext(Path projectRoot) {
|
||||
this();
|
||||
this.projectRoot = projectRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate project context summary
|
||||
*/
|
||||
public String generateContextSummary() {
|
||||
StringBuilder summary = new StringBuilder();
|
||||
|
||||
// Basic information
|
||||
summary.append("=== PROJECT CONTEXT ===\n");
|
||||
summary.append("Project: ").append(projectRoot != null ? projectRoot.getFileName() : "Unknown").append("\n");
|
||||
summary.append("Type: ").append(projectType != null ? projectType.getDisplayName() : "Unknown").append("\n");
|
||||
summary.append("Language: ").append(projectType != null ? projectType.getPrimaryLanguage() : "Unknown").append("\n");
|
||||
summary.append("Package Manager: ").append(projectType != null ? projectType.getPackageManager() : "Unknown").append("\n\n");
|
||||
|
||||
// Project structure
|
||||
if (projectStructure != null) {
|
||||
summary.append("=== PROJECT STRUCTURE ===\n");
|
||||
summary.append(projectStructure.getStructureSummary()).append("\n");
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
if (!dependencies.isEmpty()) {
|
||||
summary.append("=== DEPENDENCIES ===\n");
|
||||
dependencies.stream()
|
||||
.filter(DependencyInfo::isDirectDependency)
|
||||
.limit(10)
|
||||
.forEach(dep -> summary.append("- ").append(dep.toString()).append("\n"));
|
||||
if (dependencies.size() > 10) {
|
||||
summary.append("... and ").append(dependencies.size() - 10).append(" more dependencies\n");
|
||||
}
|
||||
summary.append("\n");
|
||||
}
|
||||
|
||||
// Configuration files
|
||||
if (!configFiles.isEmpty()) {
|
||||
summary.append("=== CONFIGURATION FILES ===\n");
|
||||
configFiles.stream()
|
||||
.filter(ConfigFile::isMainConfig)
|
||||
.forEach(config -> summary.append("- ").append(config.getFileName())
|
||||
.append(" (").append(config.getFileType()).append(")\n"));
|
||||
summary.append("\n");
|
||||
}
|
||||
|
||||
// Code statistics
|
||||
if (codeStatistics != null) {
|
||||
summary.append("=== CODE STATISTICS ===\n");
|
||||
summary.append("Total Lines: ").append(codeStatistics.getTotalLines()).append("\n");
|
||||
summary.append("Code Lines: ").append(codeStatistics.getCodeLines()).append("\n");
|
||||
if (codeStatistics.getTotalClasses() > 0) {
|
||||
summary.append("Classes: ").append(codeStatistics.getTotalClasses()).append("\n");
|
||||
}
|
||||
if (codeStatistics.getTotalMethods() > 0) {
|
||||
summary.append("Methods: ").append(codeStatistics.getTotalMethods()).append("\n");
|
||||
}
|
||||
summary.append("\n");
|
||||
}
|
||||
|
||||
this.contextSummary = summary.toString();
|
||||
return this.contextSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dependency summary
|
||||
*/
|
||||
public String getDependencySummary() {
|
||||
if (dependencies.isEmpty()) {
|
||||
return "No dependencies found";
|
||||
}
|
||||
|
||||
return dependencies.stream()
|
||||
.filter(DependencyInfo::isDirectDependency)
|
||||
.limit(5)
|
||||
.map(DependencyInfo::getName)
|
||||
.reduce((a, b) -> a + ", " + b)
|
||||
.orElse("No direct dependencies");
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Path getProjectRoot() {
|
||||
return projectRoot;
|
||||
}
|
||||
|
||||
public void setProjectRoot(Path projectRoot) {
|
||||
this.projectRoot = projectRoot;
|
||||
}
|
||||
|
||||
public ProjectType getProjectType() {
|
||||
return projectType;
|
||||
}
|
||||
|
||||
public void setProjectType(ProjectType projectType) {
|
||||
this.projectType = projectType;
|
||||
}
|
||||
|
||||
public ProjectStructure getProjectStructure() {
|
||||
return projectStructure;
|
||||
}
|
||||
|
||||
public void setProjectStructure(ProjectStructure projectStructure) {
|
||||
this.projectStructure = projectStructure;
|
||||
}
|
||||
|
||||
public List<DependencyInfo> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
public void setDependencies(List<DependencyInfo> dependencies) {
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
||||
public List<ConfigFile> getConfigFiles() {
|
||||
return configFiles;
|
||||
}
|
||||
|
||||
public void setConfigFiles(List<ConfigFile> configFiles) {
|
||||
this.configFiles = configFiles;
|
||||
}
|
||||
|
||||
public CodeStatistics getCodeStatistics() {
|
||||
return codeStatistics;
|
||||
}
|
||||
|
||||
public void setCodeStatistics(CodeStatistics codeStatistics) {
|
||||
this.codeStatistics = codeStatistics;
|
||||
}
|
||||
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public String getContextSummary() {
|
||||
return contextSummary;
|
||||
}
|
||||
|
||||
public void setContextSummary(String contextSummary) {
|
||||
this.contextSummary = contextSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependency information class
|
||||
*/
|
||||
public static class DependencyInfo {
|
||||
private String name;
|
||||
private String version;
|
||||
private String type; // "compile", "test", "runtime", etc.
|
||||
private String scope;
|
||||
private boolean isDirectDependency;
|
||||
|
||||
public DependencyInfo(String name, String version, String type) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
this.type = type;
|
||||
this.isDirectDependency = true;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public void setScope(String scope) {
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
public boolean isDirectDependency() {
|
||||
return isDirectDependency;
|
||||
}
|
||||
|
||||
public void setDirectDependency(boolean directDependency) {
|
||||
isDirectDependency = directDependency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s:%s (%s)", name, version, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration file information class
|
||||
*/
|
||||
public static class ConfigFile {
|
||||
private String fileName;
|
||||
private String relativePath;
|
||||
private String fileType; // "properties", "yaml", "json", "xml", etc.
|
||||
private Map<String, Object> keySettings;
|
||||
private boolean isMainConfig;
|
||||
|
||||
public ConfigFile(String fileName, String relativePath, String fileType) {
|
||||
this.fileName = fileName;
|
||||
this.relativePath = relativePath;
|
||||
this.fileType = fileType;
|
||||
this.keySettings = new HashMap<>();
|
||||
this.isMainConfig = false;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getRelativePath() {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
public void setRelativePath(String relativePath) {
|
||||
this.relativePath = relativePath;
|
||||
}
|
||||
|
||||
public String getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
public void setFileType(String fileType) {
|
||||
this.fileType = fileType;
|
||||
}
|
||||
|
||||
public Map<String, Object> getKeySettings() {
|
||||
return keySettings;
|
||||
}
|
||||
|
||||
public void setKeySettings(Map<String, Object> keySettings) {
|
||||
this.keySettings = keySettings;
|
||||
}
|
||||
|
||||
public boolean isMainConfig() {
|
||||
return isMainConfig;
|
||||
}
|
||||
|
||||
public void setMainConfig(boolean mainConfig) {
|
||||
isMainConfig = mainConfig;
|
||||
}
|
||||
|
||||
public void addSetting(String key, Object value) {
|
||||
this.keySettings.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Code statistics information class
|
||||
*/
|
||||
public static class CodeStatistics {
|
||||
private int totalLines;
|
||||
private int codeLines;
|
||||
private int commentLines;
|
||||
private int blankLines;
|
||||
private Map<String, Integer> languageLines;
|
||||
private int totalClasses;
|
||||
private int totalMethods;
|
||||
private int totalFunctions;
|
||||
|
||||
public CodeStatistics() {
|
||||
this.languageLines = new HashMap<>();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public int getTotalLines() {
|
||||
return totalLines;
|
||||
}
|
||||
|
||||
public void setTotalLines(int totalLines) {
|
||||
this.totalLines = totalLines;
|
||||
}
|
||||
|
||||
public int getCodeLines() {
|
||||
return codeLines;
|
||||
}
|
||||
|
||||
public void setCodeLines(int codeLines) {
|
||||
this.codeLines = codeLines;
|
||||
}
|
||||
|
||||
public int getCommentLines() {
|
||||
return commentLines;
|
||||
}
|
||||
|
||||
public void setCommentLines(int commentLines) {
|
||||
this.commentLines = commentLines;
|
||||
}
|
||||
|
||||
public int getBlankLines() {
|
||||
return blankLines;
|
||||
}
|
||||
|
||||
public void setBlankLines(int blankLines) {
|
||||
this.blankLines = blankLines;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getLanguageLines() {
|
||||
return languageLines;
|
||||
}
|
||||
|
||||
public void setLanguageLines(Map<String, Integer> languageLines) {
|
||||
this.languageLines = languageLines;
|
||||
}
|
||||
|
||||
public int getTotalClasses() {
|
||||
return totalClasses;
|
||||
}
|
||||
|
||||
public void setTotalClasses(int totalClasses) {
|
||||
this.totalClasses = totalClasses;
|
||||
}
|
||||
|
||||
public int getTotalMethods() {
|
||||
return totalMethods;
|
||||
}
|
||||
|
||||
public void setTotalMethods(int totalMethods) {
|
||||
this.totalMethods = totalMethods;
|
||||
}
|
||||
|
||||
public int getTotalFunctions() {
|
||||
return totalFunctions;
|
||||
}
|
||||
|
||||
public void setTotalFunctions(int totalFunctions) {
|
||||
this.totalFunctions = totalFunctions;
|
||||
}
|
||||
|
||||
public void addLanguageLines(String language, int lines) {
|
||||
this.languageLines.put(language, this.languageLines.getOrDefault(language, 0) + lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Project structure information model
|
||||
* Contains project directory structure, file statistics and other information
|
||||
*/
|
||||
public class ProjectStructure {
|
||||
private Path projectRoot;
|
||||
private ProjectType projectType;
|
||||
private List<DirectoryInfo> directories;
|
||||
private Map<String, Integer> fileTypeCount;
|
||||
private List<String> keyFiles;
|
||||
private int totalFiles;
|
||||
private int totalDirectories;
|
||||
private long totalSize;
|
||||
|
||||
public ProjectStructure() {
|
||||
this.directories = new ArrayList<>();
|
||||
this.fileTypeCount = new HashMap<>();
|
||||
this.keyFiles = new ArrayList<>();
|
||||
}
|
||||
|
||||
public ProjectStructure(Path projectRoot, ProjectType projectType) {
|
||||
this();
|
||||
this.projectRoot = projectRoot;
|
||||
this.projectType = projectType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add directory information
|
||||
*/
|
||||
public void addDirectory(DirectoryInfo directoryInfo) {
|
||||
this.directories.add(directoryInfo);
|
||||
this.totalDirectories++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add file type statistics
|
||||
*/
|
||||
public void addFileType(String extension, int count) {
|
||||
this.fileTypeCount.put(extension, this.fileTypeCount.getOrDefault(extension, 0) + count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add key file
|
||||
*/
|
||||
public void addKeyFile(String fileName) {
|
||||
if (!this.keyFiles.contains(fileName)) {
|
||||
this.keyFiles.add(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project structure summary
|
||||
*/
|
||||
public String getStructureSummary() {
|
||||
StringBuilder summary = new StringBuilder();
|
||||
summary.append("Project: ").append(projectRoot != null ? projectRoot.getFileName() : "Unknown").append("\n");
|
||||
summary.append("Type: ").append(projectType != null ? projectType.getDisplayName() : "Unknown").append("\n");
|
||||
summary.append("Directories: ").append(totalDirectories).append("\n");
|
||||
summary.append("Files: ").append(totalFiles).append("\n");
|
||||
|
||||
if (!keyFiles.isEmpty()) {
|
||||
summary.append("Key Files: ").append(String.join(", ", keyFiles)).append("\n");
|
||||
}
|
||||
|
||||
if (!fileTypeCount.isEmpty()) {
|
||||
summary.append("File Types: ");
|
||||
fileTypeCount.entrySet().stream()
|
||||
.sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue()))
|
||||
.limit(5)
|
||||
.forEach(entry -> summary.append(entry.getKey()).append("(").append(entry.getValue()).append(") "));
|
||||
summary.append("\n");
|
||||
}
|
||||
|
||||
return summary.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get important directories list
|
||||
*/
|
||||
public List<DirectoryInfo> getImportantDirectories() {
|
||||
return directories.stream()
|
||||
.filter(DirectoryInfo::isImportant)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark important directories based on project type
|
||||
*/
|
||||
public void markImportantDirectories() {
|
||||
if (projectType == null) return;
|
||||
|
||||
for (DirectoryInfo dir : directories) {
|
||||
String dirName = dir.getName().toLowerCase();
|
||||
|
||||
// Common important directories
|
||||
if (dirName.equals("src") || dirName.equals("source") ||
|
||||
dirName.equals("test") || dirName.equals("tests") ||
|
||||
dirName.equals("config") || dirName.equals("conf") ||
|
||||
dirName.equals("docs") || dirName.equals("doc")) {
|
||||
dir.setImportant(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Project type specific important directories
|
||||
switch (projectType) {
|
||||
case JAVA_MAVEN:
|
||||
case JAVA_GRADLE:
|
||||
case SPRING_BOOT:
|
||||
if (dirName.equals("main") || dirName.equals("resources") ||
|
||||
dirName.equals("webapp") || dirName.equals("target") ||
|
||||
dirName.equals("build")) {
|
||||
dir.setImportant(true);
|
||||
}
|
||||
break;
|
||||
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
if (dirName.equals("node_modules") || dirName.equals("public") ||
|
||||
dirName.equals("dist") || dirName.equals("build") ||
|
||||
dirName.equals("components") || dirName.equals("pages")) {
|
||||
dir.setImportant(true);
|
||||
}
|
||||
break;
|
||||
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
if (dirName.equals("venv") || dirName.equals("env") ||
|
||||
dirName.equals("__pycache__") || dirName.equals("migrations") ||
|
||||
dirName.equals("static") || dirName.equals("templates")) {
|
||||
dir.setImportant(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Path getProjectRoot() {
|
||||
return projectRoot;
|
||||
}
|
||||
|
||||
public void setProjectRoot(Path projectRoot) {
|
||||
this.projectRoot = projectRoot;
|
||||
}
|
||||
|
||||
public ProjectType getProjectType() {
|
||||
return projectType;
|
||||
}
|
||||
|
||||
public void setProjectType(ProjectType projectType) {
|
||||
this.projectType = projectType;
|
||||
}
|
||||
|
||||
public List<DirectoryInfo> getDirectories() {
|
||||
return directories;
|
||||
}
|
||||
|
||||
public void setDirectories(List<DirectoryInfo> directories) {
|
||||
this.directories = directories;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getFileTypeCount() {
|
||||
return fileTypeCount;
|
||||
}
|
||||
|
||||
public void setFileTypeCount(Map<String, Integer> fileTypeCount) {
|
||||
this.fileTypeCount = fileTypeCount;
|
||||
}
|
||||
|
||||
public List<String> getKeyFiles() {
|
||||
return keyFiles;
|
||||
}
|
||||
|
||||
public void setKeyFiles(List<String> keyFiles) {
|
||||
this.keyFiles = keyFiles;
|
||||
}
|
||||
|
||||
public int getTotalFiles() {
|
||||
return totalFiles;
|
||||
}
|
||||
|
||||
public void setTotalFiles(int totalFiles) {
|
||||
this.totalFiles = totalFiles;
|
||||
}
|
||||
|
||||
public int getTotalDirectories() {
|
||||
return totalDirectories;
|
||||
}
|
||||
|
||||
public void setTotalDirectories(int totalDirectories) {
|
||||
this.totalDirectories = totalDirectories;
|
||||
}
|
||||
|
||||
public long getTotalSize() {
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
public void setTotalSize(long totalSize) {
|
||||
this.totalSize = totalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory information inner class
|
||||
*/
|
||||
public static class DirectoryInfo {
|
||||
private String name;
|
||||
private String relativePath;
|
||||
private int fileCount;
|
||||
private List<String> files;
|
||||
private boolean isImportant; // Whether it's an important directory (like src, test, etc.)
|
||||
|
||||
public DirectoryInfo(String name, String relativePath) {
|
||||
this.name = name;
|
||||
this.relativePath = relativePath;
|
||||
this.files = new ArrayList<>();
|
||||
this.isImportant = false;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getRelativePath() {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
public void setRelativePath(String relativePath) {
|
||||
this.relativePath = relativePath;
|
||||
}
|
||||
|
||||
public int getFileCount() {
|
||||
return fileCount;
|
||||
}
|
||||
|
||||
public void setFileCount(int fileCount) {
|
||||
this.fileCount = fileCount;
|
||||
}
|
||||
|
||||
public List<String> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public void setFiles(List<String> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public boolean isImportant() {
|
||||
return isImportant;
|
||||
}
|
||||
|
||||
public void setImportant(boolean important) {
|
||||
isImportant = important;
|
||||
}
|
||||
|
||||
public void addFile(String fileName) {
|
||||
this.files.add(fileName);
|
||||
this.fileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
/**
|
||||
* Project type enumeration
|
||||
* Supports mainstream project type detection
|
||||
*/
|
||||
public enum ProjectType {
|
||||
// Java projects
|
||||
JAVA_MAVEN("Java Maven", "pom.xml", "Maven-based Java project"),
|
||||
JAVA_GRADLE("Java Gradle", "build.gradle", "Gradle-based Java project"),
|
||||
SPRING_BOOT("Spring Boot", "pom.xml", "Spring Boot application"),
|
||||
|
||||
// JavaScript/Node.js projects
|
||||
NODE_JS("Node.js", "package.json", "Node.js project"),
|
||||
REACT("React", "package.json", "React application"),
|
||||
VUE("Vue.js", "package.json", "Vue.js application"),
|
||||
ANGULAR("Angular", "package.json", "Angular application"),
|
||||
NEXT_JS("Next.js", "package.json", "Next.js application"),
|
||||
|
||||
// Python projects
|
||||
PYTHON("Python", "requirements.txt", "Python project"),
|
||||
DJANGO("Django", "manage.py", "Django web application"),
|
||||
FLASK("Flask", "app.py", "Flask web application"),
|
||||
FASTAPI("FastAPI", "main.py", "FastAPI application"),
|
||||
|
||||
// Other project types
|
||||
DOTNET("ASP.NET", "*.csproj", ".NET project"),
|
||||
GO("Go", "go.mod", "Go project"),
|
||||
RUST("Rust", "Cargo.toml", "Rust project"),
|
||||
PHP("PHP", "composer.json", "PHP project"),
|
||||
|
||||
// Web frontend
|
||||
HTML_STATIC("Static HTML", "index.html", "Static HTML website"),
|
||||
|
||||
// Unknown type
|
||||
UNKNOWN("Unknown", "", "Unknown project type");
|
||||
|
||||
private final String displayName;
|
||||
private final String keyFile;
|
||||
private final String description;
|
||||
|
||||
ProjectType(String displayName, String keyFile, String description) {
|
||||
this.displayName = displayName;
|
||||
this.keyFile = keyFile;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getKeyFile() {
|
||||
return keyFile;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a Java project
|
||||
*/
|
||||
public boolean isJavaProject() {
|
||||
return this == JAVA_MAVEN || this == JAVA_GRADLE || this == SPRING_BOOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a JavaScript project
|
||||
*/
|
||||
public boolean isJavaScriptProject() {
|
||||
return this == NODE_JS || this == REACT || this == VUE ||
|
||||
this == ANGULAR || this == NEXT_JS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a Python project
|
||||
*/
|
||||
public boolean isPythonProject() {
|
||||
return this == PYTHON || this == DJANGO || this == FLASK || this == FASTAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a Web project
|
||||
*/
|
||||
public boolean isWebProject() {
|
||||
return isJavaScriptProject() || this == HTML_STATIC ||
|
||||
this == DJANGO || this == FLASK || this == FASTAPI || this == SPRING_BOOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary programming language of the project
|
||||
*/
|
||||
public String getPrimaryLanguage() {
|
||||
if (isJavaProject()) return "Java";
|
||||
if (isJavaScriptProject()) return "JavaScript";
|
||||
if (isPythonProject()) return "Python";
|
||||
|
||||
switch (this) {
|
||||
case DOTNET:
|
||||
return "C#";
|
||||
case GO:
|
||||
return "Go";
|
||||
case RUST:
|
||||
return "Rust";
|
||||
case PHP:
|
||||
return "PHP";
|
||||
case HTML_STATIC:
|
||||
return "HTML";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recommended package manager
|
||||
*/
|
||||
public String getPackageManager() {
|
||||
switch (this) {
|
||||
case JAVA_MAVEN:
|
||||
case SPRING_BOOT:
|
||||
return "Maven";
|
||||
case JAVA_GRADLE:
|
||||
return "Gradle";
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
return "npm/yarn";
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
return "pip";
|
||||
case DOTNET:
|
||||
return "NuGet";
|
||||
case GO:
|
||||
return "go mod";
|
||||
case RUST:
|
||||
return "Cargo";
|
||||
case PHP:
|
||||
return "Composer";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TaskStatus {
|
||||
private String taskId;
|
||||
private String status; // RUNNING, COMPLETED, ERROR
|
||||
private String currentAction;
|
||||
private String summary;
|
||||
private int currentTurn;
|
||||
private int totalEstimatedTurns;
|
||||
private long startTime;
|
||||
private long lastUpdateTime;
|
||||
private List<String> actionHistory;
|
||||
private String errorMessage;
|
||||
private double progressPercentage;
|
||||
|
||||
public TaskStatus(String taskId) {
|
||||
this.taskId = taskId;
|
||||
this.status = "RUNNING";
|
||||
this.startTime = System.currentTimeMillis();
|
||||
this.lastUpdateTime = this.startTime;
|
||||
this.actionHistory = new ArrayList<>();
|
||||
this.progressPercentage = 0.0;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
this.lastUpdateTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public String getCurrentAction() {
|
||||
return currentAction;
|
||||
}
|
||||
|
||||
public void setCurrentAction(String currentAction) {
|
||||
this.currentAction = currentAction;
|
||||
this.lastUpdateTime = System.currentTimeMillis();
|
||||
if (currentAction != null && !currentAction.trim().isEmpty()) {
|
||||
this.actionHistory.add(currentAction);
|
||||
}
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public int getCurrentTurn() {
|
||||
return currentTurn;
|
||||
}
|
||||
|
||||
public void setCurrentTurn(int currentTurn) {
|
||||
this.currentTurn = currentTurn;
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
public int getTotalEstimatedTurns() {
|
||||
return totalEstimatedTurns;
|
||||
}
|
||||
|
||||
public void setTotalEstimatedTurns(int totalEstimatedTurns) {
|
||||
this.totalEstimatedTurns = totalEstimatedTurns;
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public long getLastUpdateTime() {
|
||||
return lastUpdateTime;
|
||||
}
|
||||
|
||||
public List<String> getActionHistory() {
|
||||
return actionHistory;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public double getProgressPercentage() {
|
||||
return progressPercentage;
|
||||
}
|
||||
|
||||
private void updateProgress() {
|
||||
if (totalEstimatedTurns > 0) {
|
||||
this.progressPercentage = Math.min(100.0, (double) currentTurn / totalEstimatedTurns * 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
public long getElapsedTime() {
|
||||
return System.currentTimeMillis() - startTime;
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package com.example.demo.schema;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JSON Schema definition class
|
||||
* Used to define tool parameter structure and validation rules
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class JsonSchema {
|
||||
|
||||
private String type;
|
||||
private String description;
|
||||
private String pattern;
|
||||
private Number minimum;
|
||||
private Number maximum;
|
||||
private List<Object> enumValues;
|
||||
|
||||
@JsonProperty("properties")
|
||||
private Map<String, JsonSchema> properties;
|
||||
|
||||
@JsonProperty("required")
|
||||
private List<String> requiredFields;
|
||||
|
||||
@JsonProperty("items")
|
||||
private JsonSchema items;
|
||||
|
||||
// Constructor
|
||||
public JsonSchema() {
|
||||
}
|
||||
|
||||
// Static factory methods
|
||||
public static JsonSchema object() {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "object";
|
||||
schema.properties = new HashMap<>();
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema string(String description) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "string";
|
||||
schema.description = description;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema number(String description) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "number";
|
||||
schema.description = description;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema integer(String description) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "integer";
|
||||
schema.description = description;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema bool(String description) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "boolean";
|
||||
schema.description = description;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema array(JsonSchema items) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "array";
|
||||
schema.items = items;
|
||||
return schema;
|
||||
}
|
||||
|
||||
public static JsonSchema array(String description, JsonSchema items) {
|
||||
JsonSchema schema = new JsonSchema();
|
||||
schema.type = "array";
|
||||
schema.description = description;
|
||||
schema.items = items;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// Fluent methods
|
||||
public JsonSchema addProperty(String name, JsonSchema property) {
|
||||
if (this.properties == null) {
|
||||
this.properties = new HashMap<>();
|
||||
}
|
||||
this.properties.put(name, property);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema required(String... fields) {
|
||||
this.requiredFields = Arrays.asList(fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema pattern(String pattern) {
|
||||
this.pattern = pattern;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema minimum(Number minimum) {
|
||||
this.minimum = minimum;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema maximum(Number maximum) {
|
||||
this.maximum = maximum;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchema enumValues(Object... values) {
|
||||
this.enumValues = Arrays.asList(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
public void setPattern(String pattern) {
|
||||
this.pattern = pattern;
|
||||
}
|
||||
|
||||
public Number getMinimum() {
|
||||
return minimum;
|
||||
}
|
||||
|
||||
public void setMinimum(Number minimum) {
|
||||
this.minimum = minimum;
|
||||
}
|
||||
|
||||
public Number getMaximum() {
|
||||
return maximum;
|
||||
}
|
||||
|
||||
public void setMaximum(Number maximum) {
|
||||
this.maximum = maximum;
|
||||
}
|
||||
|
||||
public List<Object> getEnumValues() {
|
||||
return enumValues;
|
||||
}
|
||||
|
||||
public void setEnumValues(List<Object> enumValues) {
|
||||
this.enumValues = enumValues;
|
||||
}
|
||||
|
||||
public Map<String, JsonSchema> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public void setProperties(Map<String, JsonSchema> properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public List<String> getRequiredFields() {
|
||||
return requiredFields;
|
||||
}
|
||||
|
||||
public void setRequiredFields(List<String> requiredFields) {
|
||||
this.requiredFields = requiredFields;
|
||||
}
|
||||
|
||||
public JsonSchema getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(JsonSchema items) {
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package com.example.demo.schema;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.networknt.schema.JsonSchemaFactory;
|
||||
import com.networknt.schema.SpecVersion;
|
||||
import com.networknt.schema.ValidationMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JSON Schema validator
|
||||
* Used to validate tool parameters against defined schema
|
||||
*/
|
||||
@Component
|
||||
public class SchemaValidator {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SchemaValidator.class);
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final JsonSchemaFactory schemaFactory;
|
||||
|
||||
public SchemaValidator() {
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate data against schema
|
||||
*
|
||||
* @param schema JSON Schema definition
|
||||
* @param data Data to validate
|
||||
* @return Validation error message, null means validation passed
|
||||
*/
|
||||
public String validate(JsonSchema schema, Object data) {
|
||||
try {
|
||||
// Convert custom JsonSchema to standard JSON Schema string
|
||||
String schemaJson = objectMapper.writeValueAsString(schema);
|
||||
logger.debug("Schema JSON: {}", schemaJson);
|
||||
|
||||
// Create JSON Schema validator
|
||||
com.networknt.schema.JsonSchema jsonSchema = schemaFactory.getSchema(schemaJson);
|
||||
|
||||
// Convert data to JsonNode
|
||||
String dataJson = objectMapper.writeValueAsString(data);
|
||||
JsonNode dataNode = objectMapper.readTree(dataJson);
|
||||
logger.debug("Data JSON: {}", dataJson);
|
||||
|
||||
// Execute validation
|
||||
Set<ValidationMessage> errors = jsonSchema.validate(dataNode);
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
logger.debug("Schema validation passed");
|
||||
return null; // Validation passed
|
||||
} else {
|
||||
String errorMessage = errors.stream()
|
||||
.map(ValidationMessage::getMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
logger.warn("Schema validation failed: {}", errorMessage);
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
String errorMessage = "Schema validation error: " + e.getMessage();
|
||||
logger.error(errorMessage, e);
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple type validation (fallback solution)
|
||||
* Used when JSON Schema validation fails
|
||||
*/
|
||||
public String validateSimple(JsonSchema schema, Object data) {
|
||||
if (schema == null || data == null) {
|
||||
return "Schema or data is null";
|
||||
}
|
||||
|
||||
// Basic type checking
|
||||
String expectedType = schema.getType();
|
||||
if (expectedType != null) {
|
||||
String actualType = getDataType(data);
|
||||
if (!isTypeCompatible(expectedType, actualType)) {
|
||||
return String.format("Type mismatch: expected %s, got %s", expectedType, actualType);
|
||||
}
|
||||
}
|
||||
|
||||
// Required field checking (only for object type)
|
||||
if ("object".equals(expectedType) && schema.getRequiredFields() != null) {
|
||||
if (!(data instanceof java.util.Map)) {
|
||||
return "Expected object type for required field validation";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Map<String, Object> dataMap = (java.util.Map<String, Object>) data;
|
||||
|
||||
for (String requiredField : schema.getRequiredFields()) {
|
||||
if (!dataMap.containsKey(requiredField) || dataMap.get(requiredField) == null) {
|
||||
return "Missing required field: " + requiredField;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Validation passed
|
||||
}
|
||||
|
||||
private String getDataType(Object data) {
|
||||
if (data == null) return "null";
|
||||
if (data instanceof String) return "string";
|
||||
if (data instanceof Integer || data instanceof Long) return "integer";
|
||||
if (data instanceof Number) return "number";
|
||||
if (data instanceof Boolean) return "boolean";
|
||||
if (data instanceof java.util.List) return "array";
|
||||
if (data instanceof java.util.Map) return "object";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private boolean isTypeCompatible(String expectedType, String actualType) {
|
||||
if (expectedType.equals(actualType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Number type compatibility
|
||||
if ("number".equals(expectedType) && "integer".equals(actualType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,80 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
/**
|
||||
* 日志事件基类
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class LogEvent {
|
||||
|
||||
private String type;
|
||||
private String taskId;
|
||||
private String message;
|
||||
private String timestamp;
|
||||
|
||||
// Constructors
|
||||
public LogEvent() {
|
||||
}
|
||||
|
||||
public LogEvent(String type, String taskId, String message, String timestamp) {
|
||||
this.type = type;
|
||||
this.taskId = taskId;
|
||||
this.message = message;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
// Static factory methods
|
||||
public static LogEvent createConnectionEvent(String taskId) {
|
||||
LogEvent event = new LogEvent();
|
||||
event.setType("CONNECTION_ESTABLISHED");
|
||||
event.setTaskId(taskId);
|
||||
event.setMessage("SSE连接已建立");
|
||||
event.setTimestamp(java.time.LocalDateTime.now().format(
|
||||
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
return event;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(String timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LogEvent{" +
|
||||
"type='" + type + '\'' +
|
||||
", taskId='" + taskId + '\'' +
|
||||
", message='" + message + '\'' +
|
||||
", timestamp='" + timestamp + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* SSE日志推送服务
|
||||
* 负责将AOP日志实时推送到前端
|
||||
*/
|
||||
@Service
|
||||
public class LogStreamService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LogStreamService.class);
|
||||
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
// 活跃的SSE连接 taskId -> SseEmitter
|
||||
private final Map<String, SseEmitter> activeConnections = new ConcurrentHashMap<>();
|
||||
|
||||
// JSON序列化器
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 建立SSE连接
|
||||
*/
|
||||
public SseEmitter createConnection(String taskId) {
|
||||
logger.info("🔗 建立SSE连接: taskId={}", taskId);
|
||||
|
||||
SseEmitter emitter = new SseEmitter(0L); // 无超时
|
||||
|
||||
// 设置连接事件处理
|
||||
emitter.onCompletion(() -> {
|
||||
logger.info("✅ SSE连接完成: taskId={}", taskId);
|
||||
activeConnections.remove(taskId);
|
||||
});
|
||||
|
||||
emitter.onTimeout(() -> {
|
||||
logger.warn("⏰ SSE连接超时: taskId={}", taskId);
|
||||
activeConnections.remove(taskId);
|
||||
});
|
||||
|
||||
emitter.onError((ex) -> {
|
||||
logger.error("❌ SSE连接错误: taskId={}, error={}", taskId, ex.getMessage());
|
||||
activeConnections.remove(taskId);
|
||||
});
|
||||
|
||||
// 保存连接
|
||||
activeConnections.put(taskId, emitter);
|
||||
|
||||
// 发送连接成功消息
|
||||
sendLogEvent(taskId, LogEvent.createConnectionEvent(taskId));
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭SSE连接
|
||||
*/
|
||||
public void closeConnection(String taskId) {
|
||||
SseEmitter emitter = activeConnections.remove(taskId);
|
||||
if (emitter != null) {
|
||||
try {
|
||||
emitter.complete();
|
||||
logger.info("🔚 关闭SSE连接: taskId={}", taskId);
|
||||
} catch (Exception e) {
|
||||
logger.error("关闭SSE连接失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送工具开始执行事件
|
||||
*/
|
||||
public void pushToolStart(String taskId, String toolName, String filePath, String message) {
|
||||
ToolLogEvent event = new ToolLogEvent();
|
||||
event.setType("TOOL_START");
|
||||
event.setTaskId(taskId);
|
||||
event.setToolName(toolName);
|
||||
event.setFilePath(filePath);
|
||||
event.setMessage(message);
|
||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||
event.setIcon(getToolIcon(toolName));
|
||||
event.setStatus("RUNNING");
|
||||
|
||||
sendLogEvent(taskId, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送工具执行成功事件
|
||||
*/
|
||||
public void pushToolSuccess(String taskId, String toolName, String filePath, String message, long executionTime) {
|
||||
ToolLogEvent event = new ToolLogEvent();
|
||||
event.setType("TOOL_SUCCESS");
|
||||
event.setTaskId(taskId);
|
||||
event.setToolName(toolName);
|
||||
event.setFilePath(filePath);
|
||||
event.setMessage(message);
|
||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||
event.setIcon(getToolIcon(toolName));
|
||||
event.setStatus("SUCCESS");
|
||||
event.setExecutionTime(executionTime);
|
||||
|
||||
sendLogEvent(taskId, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送工具执行失败事件
|
||||
*/
|
||||
public void pushToolError(String taskId, String toolName, String filePath, String message, long executionTime) {
|
||||
ToolLogEvent event = new ToolLogEvent();
|
||||
event.setType("TOOL_ERROR");
|
||||
event.setTaskId(taskId);
|
||||
event.setToolName(toolName);
|
||||
event.setFilePath(filePath);
|
||||
event.setMessage(message);
|
||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||
event.setIcon("❌");
|
||||
event.setStatus("ERROR");
|
||||
event.setExecutionTime(executionTime);
|
||||
|
||||
sendLogEvent(taskId, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送任务完成事件
|
||||
*/
|
||||
public void pushTaskComplete(String taskId) {
|
||||
LogEvent event = new LogEvent();
|
||||
event.setType("TASK_COMPLETE");
|
||||
event.setTaskId(taskId);
|
||||
event.setMessage("任务执行完成");
|
||||
event.setTimestamp(LocalDateTime.now().format(formatter));
|
||||
|
||||
sendLogEvent(taskId, event);
|
||||
|
||||
// 延迟关闭连接
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(2000); // 等待2秒让前端处理完成事件
|
||||
closeConnection(taskId);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送日志事件到前端
|
||||
*/
|
||||
private void sendLogEvent(String taskId, Object event) {
|
||||
SseEmitter emitter = activeConnections.get(taskId);
|
||||
if (emitter != null) {
|
||||
try {
|
||||
String jsonData = objectMapper.writeValueAsString(event);
|
||||
logger.info("📤 准备推送日志事件: taskId={}, type={}, data={}", taskId,
|
||||
event instanceof LogEvent ? ((LogEvent) event).getType() : "unknown", jsonData);
|
||||
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("log")
|
||||
.data(jsonData));
|
||||
|
||||
logger.info("✅ 日志事件推送成功: taskId={}", taskId);
|
||||
} catch (IOException e) {
|
||||
logger.error("推送日志事件失败: taskId={}, error={}", taskId, e.getMessage());
|
||||
activeConnections.remove(taskId);
|
||||
}
|
||||
} else {
|
||||
logger.warn("⚠️ 未找到SSE连接: taskId={}, 无法推送事件", taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具图标
|
||||
*/
|
||||
private String getToolIcon(String toolName) {
|
||||
switch (toolName) {
|
||||
case "readFile":
|
||||
return "📖";
|
||||
case "writeFile":
|
||||
return "✏️";
|
||||
case "editFile":
|
||||
return "📝";
|
||||
case "listDirectory":
|
||||
return "📁";
|
||||
case "analyzeProject":
|
||||
return "🔍";
|
||||
case "scaffoldProject":
|
||||
return "🏗️";
|
||||
case "smartEdit":
|
||||
return "🧠";
|
||||
default:
|
||||
return "⚙️";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃连接数
|
||||
*/
|
||||
public int getActiveConnectionCount() {
|
||||
return activeConnections.size();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,354 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectContext;
|
||||
import com.example.demo.model.ProjectStructure;
|
||||
import com.example.demo.model.ProjectType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 项目上下文分析器
|
||||
* 提供完整的项目分析功能,生成AI可理解的项目上下文
|
||||
*/
|
||||
@Service
|
||||
public class ProjectContextAnalyzer {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProjectContextAnalyzer.class);
|
||||
|
||||
@Autowired
|
||||
public ProjectTypeDetector projectTypeDetector;
|
||||
|
||||
@Autowired
|
||||
public ProjectDiscoveryService projectDiscoveryService;
|
||||
|
||||
/**
|
||||
* 分析项目并生成完整上下文
|
||||
*
|
||||
* @param projectRoot 项目根目录
|
||||
* @return 项目上下文信息
|
||||
*/
|
||||
public ProjectContext analyzeProject(Path projectRoot) {
|
||||
logger.info("Starting comprehensive project analysis for: {}", projectRoot);
|
||||
|
||||
ProjectContext context = new ProjectContext(projectRoot);
|
||||
|
||||
try {
|
||||
// 1. 检测项目类型
|
||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||
context.setProjectType(projectType);
|
||||
logger.debug("Detected project type: {}", projectType);
|
||||
|
||||
// 2. 分析项目结构
|
||||
ProjectStructure structure = projectDiscoveryService.analyzeProjectStructure(projectRoot);
|
||||
context.setProjectStructure(structure);
|
||||
logger.debug("Analyzed project structure with {} directories",
|
||||
structure.getDirectories().size());
|
||||
|
||||
// 3. 分析依赖关系
|
||||
List<ProjectContext.DependencyInfo> dependencies =
|
||||
projectDiscoveryService.analyzeDependencies(projectRoot);
|
||||
context.setDependencies(dependencies);
|
||||
logger.debug("Found {} dependencies", dependencies.size());
|
||||
|
||||
// 4. 查找配置文件
|
||||
List<ProjectContext.ConfigFile> configFiles =
|
||||
projectDiscoveryService.findConfigurationFiles(projectRoot);
|
||||
context.setConfigFiles(configFiles);
|
||||
logger.debug("Found {} configuration files", configFiles.size());
|
||||
|
||||
// 5. 分析代码统计
|
||||
ProjectContext.CodeStatistics codeStats = analyzeCodeStatistics(projectRoot, projectType);
|
||||
context.setCodeStatistics(codeStats);
|
||||
logger.debug("Code statistics: {} total lines", codeStats.getTotalLines());
|
||||
|
||||
// 6. 收集项目元数据
|
||||
Map<String, Object> metadata = collectProjectMetadata(projectRoot, projectType);
|
||||
context.setMetadata(metadata);
|
||||
|
||||
// 7. 生成上下文摘要
|
||||
String summary = context.generateContextSummary();
|
||||
logger.debug("Generated context summary with {} characters", summary.length());
|
||||
|
||||
logger.info("Project analysis completed successfully for: {}", projectRoot);
|
||||
return context;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error during project analysis for: " + projectRoot, e);
|
||||
// 返回部分分析结果
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析代码统计信息
|
||||
*/
|
||||
private ProjectContext.CodeStatistics analyzeCodeStatistics(Path projectRoot, ProjectType projectType) {
|
||||
logger.debug("Analyzing code statistics for: {}", projectRoot);
|
||||
|
||||
ProjectContext.CodeStatistics stats = new ProjectContext.CodeStatistics();
|
||||
|
||||
try {
|
||||
analyzeCodeInDirectory(projectRoot, stats, projectType, 0, 3);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error analyzing code statistics", e);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归分析目录中的代码
|
||||
*/
|
||||
private void analyzeCodeInDirectory(Path directory, ProjectContext.CodeStatistics stats,
|
||||
ProjectType projectType, int currentDepth, int maxDepth) {
|
||||
if (currentDepth > maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Stream<Path> paths = Files.list(directory)) {
|
||||
paths.forEach(path -> {
|
||||
try {
|
||||
if (Files.isDirectory(path)) {
|
||||
String dirName = path.getFileName().toString();
|
||||
// 跳过不需要分析的目录
|
||||
if (!shouldSkipDirectory(dirName)) {
|
||||
analyzeCodeInDirectory(path, stats, projectType, currentDepth + 1, maxDepth);
|
||||
}
|
||||
} else if (Files.isRegularFile(path)) {
|
||||
analyzeCodeFile(path, stats, projectType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error processing path during code analysis: " + path, e);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error listing directory: " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析单个代码文件
|
||||
*/
|
||||
private void analyzeCodeFile(Path filePath, ProjectContext.CodeStatistics stats, ProjectType projectType) {
|
||||
String fileName = filePath.getFileName().toString();
|
||||
String extension = getFileExtension(fileName).toLowerCase();
|
||||
|
||||
// 只分析代码文件
|
||||
if (!isCodeFile(extension, projectType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(filePath);
|
||||
int totalLines = lines.size();
|
||||
int codeLines = 0;
|
||||
int commentLines = 0;
|
||||
int blankLines = 0;
|
||||
|
||||
for (String line : lines) {
|
||||
String trimmedLine = line.trim();
|
||||
if (trimmedLine.isEmpty()) {
|
||||
blankLines++;
|
||||
} else if (isCommentLine(trimmedLine, extension)) {
|
||||
commentLines++;
|
||||
} else {
|
||||
codeLines++;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
stats.setTotalLines(stats.getTotalLines() + totalLines);
|
||||
stats.setCodeLines(stats.getCodeLines() + codeLines);
|
||||
stats.setCommentLines(stats.getCommentLines() + commentLines);
|
||||
stats.setBlankLines(stats.getBlankLines() + blankLines);
|
||||
|
||||
// 按语言统计
|
||||
String language = getLanguageByExtension(extension);
|
||||
stats.addLanguageLines(language, totalLines);
|
||||
|
||||
// 分析类和方法(简单实现)
|
||||
if (extension.equals(".java")) {
|
||||
analyzeJavaFile(lines, stats);
|
||||
} else if (extension.equals(".js") || extension.equals(".ts")) {
|
||||
analyzeJavaScriptFile(lines, stats);
|
||||
} else if (extension.equals(".py")) {
|
||||
analyzePythonFile(lines, stats);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading file for code analysis: " + filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Java文件
|
||||
*/
|
||||
private void analyzeJavaFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
||||
for (String line : lines) {
|
||||
String trimmedLine = line.trim();
|
||||
if (trimmedLine.matches(".*\\bclass\\s+\\w+.*")) {
|
||||
stats.setTotalClasses(stats.getTotalClasses() + 1);
|
||||
}
|
||||
if (trimmedLine.matches(".*\\b(public|private|protected)\\s+.*\\s+\\w+\\s*\\(.*\\).*")) {
|
||||
stats.setTotalMethods(stats.getTotalMethods() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析JavaScript文件
|
||||
*/
|
||||
private void analyzeJavaScriptFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
||||
for (String line : lines) {
|
||||
String trimmedLine = line.trim();
|
||||
if (trimmedLine.matches(".*\\bfunction\\s+\\w+.*") ||
|
||||
trimmedLine.matches(".*\\w+\\s*:\\s*function.*") ||
|
||||
trimmedLine.matches(".*\\w+\\s*=\\s*\\(.*\\)\\s*=>.*")) {
|
||||
stats.setTotalFunctions(stats.getTotalFunctions() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Python文件
|
||||
*/
|
||||
private void analyzePythonFile(List<String> lines, ProjectContext.CodeStatistics stats) {
|
||||
for (String line : lines) {
|
||||
String trimmedLine = line.trim();
|
||||
if (trimmedLine.matches("^class\\s+\\w+.*:")) {
|
||||
stats.setTotalClasses(stats.getTotalClasses() + 1);
|
||||
}
|
||||
if (trimmedLine.matches("^def\\s+\\w+.*:")) {
|
||||
stats.setTotalFunctions(stats.getTotalFunctions() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集项目元数据
|
||||
*/
|
||||
private Map<String, Object> collectProjectMetadata(Path projectRoot, ProjectType projectType) {
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
|
||||
metadata.put("projectName", projectRoot.getFileName().toString());
|
||||
metadata.put("projectType", projectType.name());
|
||||
metadata.put("primaryLanguage", projectType.getPrimaryLanguage());
|
||||
metadata.put("packageManager", projectType.getPackageManager());
|
||||
metadata.put("analysisTimestamp", System.currentTimeMillis());
|
||||
|
||||
// 检查版本控制
|
||||
if (Files.exists(projectRoot.resolve(".git"))) {
|
||||
metadata.put("versionControl", "Git");
|
||||
}
|
||||
|
||||
// 检查CI/CD配置
|
||||
if (Files.exists(projectRoot.resolve(".github"))) {
|
||||
metadata.put("cicd", "GitHub Actions");
|
||||
} else if (Files.exists(projectRoot.resolve(".gitlab-ci.yml"))) {
|
||||
metadata.put("cicd", "GitLab CI");
|
||||
}
|
||||
|
||||
// 检查Docker支持
|
||||
if (Files.exists(projectRoot.resolve("Dockerfile"))) {
|
||||
metadata.put("containerization", "Docker");
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成编辑上下文
|
||||
*/
|
||||
public String buildEditContext(Path projectRoot, String editDescription) {
|
||||
logger.debug("Building edit context for: {}", projectRoot);
|
||||
|
||||
ProjectContext context = analyzeProject(projectRoot);
|
||||
|
||||
StringBuilder contextBuilder = new StringBuilder();
|
||||
contextBuilder.append("=== EDIT CONTEXT ===\n");
|
||||
contextBuilder.append("Edit Request: ").append(editDescription).append("\n\n");
|
||||
contextBuilder.append(context.generateContextSummary());
|
||||
|
||||
return contextBuilder.toString();
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private boolean shouldSkipDirectory(String dirName) {
|
||||
return dirName.equals(".git") || dirName.equals("node_modules") ||
|
||||
dirName.equals("target") || dirName.equals("build") ||
|
||||
dirName.equals("dist") || dirName.equals("__pycache__") ||
|
||||
dirName.startsWith(".");
|
||||
}
|
||||
|
||||
private String getFileExtension(String fileName) {
|
||||
int lastDot = fileName.lastIndexOf('.');
|
||||
return lastDot > 0 ? fileName.substring(lastDot) : "";
|
||||
}
|
||||
|
||||
private boolean isCodeFile(String extension, ProjectType projectType) {
|
||||
return extension.equals(".java") || extension.equals(".js") || extension.equals(".ts") ||
|
||||
extension.equals(".py") || extension.equals(".html") || extension.equals(".css") ||
|
||||
extension.equals(".jsx") || extension.equals(".tsx") || extension.equals(".vue") ||
|
||||
extension.equals(".go") || extension.equals(".rs") || extension.equals(".php") ||
|
||||
extension.equals(".cs") || extension.equals(".cpp") || extension.equals(".c");
|
||||
}
|
||||
|
||||
private boolean isCommentLine(String line, String extension) {
|
||||
switch (extension) {
|
||||
case ".java":
|
||||
case ".js":
|
||||
case ".ts":
|
||||
case ".jsx":
|
||||
case ".tsx":
|
||||
case ".css":
|
||||
return line.startsWith("//") || line.startsWith("/*") || line.startsWith("*");
|
||||
case ".py":
|
||||
return line.startsWith("#");
|
||||
case ".html":
|
||||
return line.startsWith("<!--");
|
||||
default:
|
||||
return line.startsWith("#") || line.startsWith("//");
|
||||
}
|
||||
}
|
||||
|
||||
private String getLanguageByExtension(String extension) {
|
||||
switch (extension) {
|
||||
case ".java":
|
||||
return "Java";
|
||||
case ".js":
|
||||
case ".jsx":
|
||||
return "JavaScript";
|
||||
case ".ts":
|
||||
case ".tsx":
|
||||
return "TypeScript";
|
||||
case ".py":
|
||||
return "Python";
|
||||
case ".html":
|
||||
return "HTML";
|
||||
case ".css":
|
||||
return "CSS";
|
||||
case ".vue":
|
||||
return "Vue";
|
||||
case ".go":
|
||||
return "Go";
|
||||
case ".rs":
|
||||
return "Rust";
|
||||
case ".php":
|
||||
return "PHP";
|
||||
case ".cs":
|
||||
return "C#";
|
||||
default:
|
||||
return "Other";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectContext;
|
||||
import com.example.demo.model.ProjectStructure;
|
||||
import com.example.demo.model.ProjectType;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 项目发现和分析服务
|
||||
* 负责分析项目结构、依赖关系和配置信息
|
||||
*/
|
||||
@Service
|
||||
public class ProjectDiscoveryService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProjectDiscoveryService.class);
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
@Autowired
|
||||
private ProjectTypeDetector projectTypeDetector;
|
||||
|
||||
/**
|
||||
* 分析项目结构
|
||||
*
|
||||
* @param projectRoot 项目根目录
|
||||
* @return 项目结构信息
|
||||
*/
|
||||
public ProjectStructure analyzeProjectStructure(Path projectRoot) {
|
||||
logger.debug("Analyzing project structure for: {}", projectRoot);
|
||||
|
||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||
ProjectStructure structure = new ProjectStructure(projectRoot, projectType);
|
||||
|
||||
try {
|
||||
analyzeDirectoryStructure(projectRoot, structure, 0, 3); // 最大深度3层
|
||||
structure.markImportantDirectories();
|
||||
|
||||
logger.info("Project structure analysis completed for: {}", projectRoot);
|
||||
return structure;
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Error analyzing project structure for: " + projectRoot, e);
|
||||
return structure; // 返回部分分析结果
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归分析目录结构
|
||||
*/
|
||||
private void analyzeDirectoryStructure(Path currentPath, ProjectStructure structure,
|
||||
int currentDepth, int maxDepth) throws IOException {
|
||||
if (currentDepth > maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Stream<Path> paths = Files.list(currentPath)) {
|
||||
paths.forEach(path -> {
|
||||
try {
|
||||
if (Files.isDirectory(path)) {
|
||||
String dirName = path.getFileName().toString();
|
||||
String relativePath = structure.getProjectRoot().relativize(path).toString();
|
||||
|
||||
// 跳过常见的忽略目录
|
||||
if (shouldIgnoreDirectory(dirName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProjectStructure.DirectoryInfo dirInfo =
|
||||
new ProjectStructure.DirectoryInfo(dirName, relativePath);
|
||||
|
||||
// 分析目录中的文件
|
||||
analyzeDirectoryFiles(path, dirInfo);
|
||||
structure.addDirectory(dirInfo);
|
||||
|
||||
// 递归分析子目录
|
||||
if (currentDepth < maxDepth) {
|
||||
analyzeDirectoryStructure(path, structure, currentDepth + 1, maxDepth);
|
||||
}
|
||||
|
||||
} else if (Files.isRegularFile(path)) {
|
||||
// 处理根目录下的文件
|
||||
String fileName = path.getFileName().toString();
|
||||
String extension = getFileExtension(fileName);
|
||||
|
||||
structure.addFileType(extension, 1);
|
||||
structure.setTotalFiles(structure.getTotalFiles() + 1);
|
||||
|
||||
// 检查是否为关键文件
|
||||
if (isKeyFile(fileName, structure.getProjectType())) {
|
||||
structure.addKeyFile(fileName);
|
||||
}
|
||||
|
||||
// 累计文件大小
|
||||
try {
|
||||
structure.setTotalSize(structure.getTotalSize() + Files.size(path));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not get size for file: {}", path);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error processing path: " + path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析目录中的文件
|
||||
*/
|
||||
private void analyzeDirectoryFiles(Path directory, ProjectStructure.DirectoryInfo dirInfo) {
|
||||
try (Stream<Path> files = Files.list(directory)) {
|
||||
files.filter(Files::isRegularFile)
|
||||
.forEach(file -> {
|
||||
String fileName = file.getFileName().toString();
|
||||
dirInfo.addFile(fileName);
|
||||
});
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error analyzing files in directory: {}", directory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析项目依赖
|
||||
*/
|
||||
public List<ProjectContext.DependencyInfo> analyzeDependencies(Path projectRoot) {
|
||||
logger.debug("Analyzing dependencies for: {}", projectRoot);
|
||||
|
||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||
|
||||
try {
|
||||
switch (projectType) {
|
||||
case JAVA_MAVEN:
|
||||
case SPRING_BOOT:
|
||||
dependencies.addAll(analyzeMavenDependencies(projectRoot));
|
||||
break;
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
dependencies.addAll(analyzeNpmDependencies(projectRoot));
|
||||
break;
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
dependencies.addAll(analyzePythonDependencies(projectRoot));
|
||||
break;
|
||||
default:
|
||||
logger.info("Dependency analysis not supported for project type: {}", projectType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error analyzing dependencies for: " + projectRoot, e);
|
||||
}
|
||||
|
||||
logger.info("Found {} dependencies for project: {}", dependencies.size(), projectRoot);
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Maven依赖
|
||||
*/
|
||||
private List<ProjectContext.DependencyInfo> analyzeMavenDependencies(Path projectRoot) {
|
||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||
Path pomFile = projectRoot.resolve("pom.xml");
|
||||
|
||||
if (!Files.exists(pomFile)) {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
try {
|
||||
String pomContent = Files.readString(pomFile);
|
||||
// 简单的XML解析 - 在实际项目中应该使用专门的XML解析器
|
||||
if (pomContent.contains("spring-boot-starter-web")) {
|
||||
dependencies.add(new ProjectContext.DependencyInfo(
|
||||
"spring-boot-starter-web", "auto", "compile"));
|
||||
}
|
||||
if (pomContent.contains("spring-boot-starter-data-jpa")) {
|
||||
dependencies.add(new ProjectContext.DependencyInfo(
|
||||
"spring-boot-starter-data-jpa", "auto", "compile"));
|
||||
}
|
||||
if (pomContent.contains("spring-boot-starter-test")) {
|
||||
dependencies.add(new ProjectContext.DependencyInfo(
|
||||
"spring-boot-starter-test", "auto", "test"));
|
||||
}
|
||||
// 可以添加更多依赖检测逻辑
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading pom.xml", e);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析NPM依赖
|
||||
*/
|
||||
private List<ProjectContext.DependencyInfo> analyzeNpmDependencies(Path projectRoot) {
|
||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||
Path packageJsonPath = projectRoot.resolve("package.json");
|
||||
|
||||
if (!Files.exists(packageJsonPath)) {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
try {
|
||||
String content = Files.readString(packageJsonPath);
|
||||
JsonNode packageJson = objectMapper.readTree(content);
|
||||
|
||||
// 分析生产依赖
|
||||
JsonNode deps = packageJson.get("dependencies");
|
||||
if (deps != null) {
|
||||
deps.fields().forEachRemaining(entry -> {
|
||||
dependencies.add(new ProjectContext.DependencyInfo(
|
||||
entry.getKey(), entry.getValue().asText(), "production"));
|
||||
});
|
||||
}
|
||||
|
||||
// 分析开发依赖
|
||||
JsonNode devDeps = packageJson.get("devDependencies");
|
||||
if (devDeps != null) {
|
||||
devDeps.fields().forEachRemaining(entry -> {
|
||||
ProjectContext.DependencyInfo depInfo = new ProjectContext.DependencyInfo(
|
||||
entry.getKey(), entry.getValue().asText(), "development");
|
||||
depInfo.setDirectDependency(true);
|
||||
dependencies.add(depInfo);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading package.json", e);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Python依赖
|
||||
*/
|
||||
private List<ProjectContext.DependencyInfo> analyzePythonDependencies(Path projectRoot) {
|
||||
List<ProjectContext.DependencyInfo> dependencies = new ArrayList<>();
|
||||
Path requirementsFile = projectRoot.resolve("requirements.txt");
|
||||
|
||||
if (Files.exists(requirementsFile)) {
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(requirementsFile);
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty() && !line.startsWith("#")) {
|
||||
String[] parts = line.split("==|>=|<=|>|<");
|
||||
String name = parts[0].trim();
|
||||
String version = parts.length > 1 ? parts[1].trim() : "latest";
|
||||
dependencies.add(new ProjectContext.DependencyInfo(name, version, "runtime"));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading requirements.txt", e);
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找配置文件
|
||||
*/
|
||||
public List<ProjectContext.ConfigFile> findConfigurationFiles(Path projectRoot) {
|
||||
logger.debug("Finding configuration files for: {}", projectRoot);
|
||||
|
||||
List<ProjectContext.ConfigFile> configFiles = new ArrayList<>();
|
||||
ProjectType projectType = projectTypeDetector.detectProjectType(projectRoot);
|
||||
|
||||
try {
|
||||
// 通用配置文件
|
||||
addConfigFileIfExists(configFiles, projectRoot, "application.properties", "properties");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "application.yml", "yaml");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "application.yaml", "yaml");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "config.json", "json");
|
||||
|
||||
// 项目类型特定的配置文件
|
||||
switch (projectType) {
|
||||
case JAVA_MAVEN:
|
||||
case SPRING_BOOT:
|
||||
addConfigFileIfExists(configFiles, projectRoot, "pom.xml", "xml");
|
||||
break;
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
addConfigFileIfExists(configFiles, projectRoot, "package.json", "json");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "webpack.config.js", "javascript");
|
||||
break;
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
addConfigFileIfExists(configFiles, projectRoot, "requirements.txt", "text");
|
||||
addConfigFileIfExists(configFiles, projectRoot, "setup.py", "python");
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error finding configuration files for: " + projectRoot, e);
|
||||
}
|
||||
|
||||
logger.info("Found {} configuration files for project: {}", configFiles.size(), projectRoot);
|
||||
return configFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加配置文件(如果存在)
|
||||
*/
|
||||
private void addConfigFileIfExists(List<ProjectContext.ConfigFile> configFiles,
|
||||
Path projectRoot, String fileName, String fileType) {
|
||||
Path configPath = projectRoot.resolve(fileName);
|
||||
if (Files.exists(configPath)) {
|
||||
String relativePath = projectRoot.relativize(configPath).toString();
|
||||
ProjectContext.ConfigFile configFile =
|
||||
new ProjectContext.ConfigFile(fileName, relativePath, fileType);
|
||||
|
||||
// 标记主要配置文件
|
||||
if (fileName.equals("pom.xml") || fileName.equals("package.json") ||
|
||||
fileName.startsWith("application.")) {
|
||||
configFile.setMainConfig(true);
|
||||
}
|
||||
|
||||
configFiles.add(configFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该忽略目录
|
||||
*/
|
||||
private boolean shouldIgnoreDirectory(String dirName) {
|
||||
return dirName.equals(".git") || dirName.equals(".svn") ||
|
||||
dirName.equals("node_modules") || dirName.equals("target") ||
|
||||
dirName.equals("build") || dirName.equals("dist") ||
|
||||
dirName.equals("__pycache__") || dirName.equals(".idea") ||
|
||||
dirName.equals(".vscode") || dirName.startsWith(".");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
private String getFileExtension(String fileName) {
|
||||
int lastDot = fileName.lastIndexOf('.');
|
||||
return lastDot > 0 ? fileName.substring(lastDot) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为关键文件
|
||||
*/
|
||||
private boolean isKeyFile(String fileName, ProjectType projectType) {
|
||||
// 通用关键文件
|
||||
if (fileName.equals("README.md") || fileName.equals("LICENSE") ||
|
||||
fileName.equals("Dockerfile") || fileName.equals(".gitignore")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 项目类型特定的关键文件
|
||||
if (projectType != null) {
|
||||
String keyFile = projectType.getKeyFile();
|
||||
if (keyFile != null && !keyFile.isEmpty()) {
|
||||
return fileName.equals(keyFile) || fileName.matches(keyFile);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,453 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectType;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 项目模板扩展服务
|
||||
* 生成README、gitignore等通用文件模板
|
||||
*/
|
||||
@Service
|
||||
public class ProjectTemplateExtensions {
|
||||
|
||||
/**
|
||||
* 生成README.md内容
|
||||
*/
|
||||
public String generateReadmeContent(Map<String, String> variables) {
|
||||
return String.format("""
|
||||
# %s
|
||||
|
||||
%s
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Java 17 or higher (for Java projects)
|
||||
- Node.js 16+ (for JavaScript projects)
|
||||
- Python 3.8+ (for Python projects)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd %s
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
# For Java Maven projects
|
||||
mvn clean install
|
||||
|
||||
# For Node.js projects
|
||||
npm install
|
||||
|
||||
# For Python projects
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Run the application:
|
||||
```bash
|
||||
# For Java Maven projects
|
||||
mvn spring-boot:run
|
||||
|
||||
# For Node.js projects
|
||||
npm start
|
||||
|
||||
# For Python projects
|
||||
python main.py
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
%s/
|
||||
├── src/ # Source code
|
||||
├── test/ # Test files
|
||||
├── docs/ # Documentation
|
||||
├── README.md # This file
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# For Java projects
|
||||
mvn test
|
||||
|
||||
# For Node.js projects
|
||||
npm test
|
||||
|
||||
# For Python projects
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# For Java projects
|
||||
mvn clean package
|
||||
|
||||
# For Node.js projects
|
||||
npm run build
|
||||
|
||||
# For Python projects
|
||||
python setup.py build
|
||||
```
|
||||
|
||||
## 📝 Features
|
||||
|
||||
- Feature 1: Description
|
||||
- Feature 2: Description
|
||||
- Feature 3: Description
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the project
|
||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 👥 Authors
|
||||
|
||||
- **%s** - *Initial work* - [%s](mailto:%s)
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Hat tip to anyone whose code was used
|
||||
- Inspiration
|
||||
- etc
|
||||
|
||||
---
|
||||
|
||||
Created with ❤️ by %s
|
||||
""",
|
||||
variables.get("PROJECT_NAME_PASCAL"),
|
||||
variables.get("DESCRIPTION"),
|
||||
variables.get("PROJECT_NAME"),
|
||||
variables.get("PROJECT_NAME"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("AUTHOR"),
|
||||
variables.get("EMAIL"),
|
||||
variables.get("AUTHOR")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成.gitignore内容
|
||||
*/
|
||||
public String generateGitignoreContent(ProjectType projectType) {
|
||||
StringBuilder gitignore = new StringBuilder();
|
||||
|
||||
// 通用忽略规则
|
||||
gitignore.append("""
|
||||
# General
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
""");
|
||||
|
||||
// 项目类型特定的忽略规则
|
||||
switch (projectType) {
|
||||
case JAVA_MAVEN:
|
||||
case JAVA_GRADLE:
|
||||
case SPRING_BOOT:
|
||||
gitignore.append("""
|
||||
# Java
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.nar
|
||||
hs_err_pid*
|
||||
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# Spring Boot
|
||||
spring-boot-*.log
|
||||
|
||||
""");
|
||||
break;
|
||||
|
||||
case NODE_JS:
|
||||
case REACT:
|
||||
case VUE:
|
||||
case ANGULAR:
|
||||
case NEXT_JS:
|
||||
gitignore.append("""
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# parcel-bundler cache
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
""");
|
||||
break;
|
||||
|
||||
case PYTHON:
|
||||
case DJANGO:
|
||||
case FLASK:
|
||||
case FASTAPI:
|
||||
gitignore.append("""
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
""");
|
||||
break;
|
||||
|
||||
default:
|
||||
// 基本忽略规则已经添加
|
||||
break;
|
||||
}
|
||||
|
||||
return gitignore.toString();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,285 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.ProjectType;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 项目类型检测器
|
||||
* 基于文件特征自动识别项目类型
|
||||
*/
|
||||
@Component
|
||||
public class ProjectTypeDetector {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProjectTypeDetector.class);
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 检测项目类型
|
||||
*
|
||||
* @param projectRoot 项目根目录
|
||||
* @return 检测到的项目类型
|
||||
*/
|
||||
public ProjectType detectProjectType(Path projectRoot) {
|
||||
if (!Files.exists(projectRoot) || !Files.isDirectory(projectRoot)) {
|
||||
logger.warn("Project root does not exist or is not a directory: {}", projectRoot);
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("Detecting project type for: {}", projectRoot);
|
||||
|
||||
// 按优先级检测项目类型
|
||||
ProjectType detectedType = detectByKeyFiles(projectRoot);
|
||||
if (detectedType != ProjectType.UNKNOWN) {
|
||||
logger.info("Detected project type: {} for {}", detectedType, projectRoot);
|
||||
return detectedType;
|
||||
}
|
||||
|
||||
// 如果关键文件检测失败,尝试基于目录结构检测
|
||||
detectedType = detectByDirectoryStructure(projectRoot);
|
||||
if (detectedType != ProjectType.UNKNOWN) {
|
||||
logger.info("Detected project type by structure: {} for {}", detectedType, projectRoot);
|
||||
return detectedType;
|
||||
}
|
||||
|
||||
logger.info("Could not determine project type for: {}", projectRoot);
|
||||
return ProjectType.UNKNOWN;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error detecting project type for: " + projectRoot, e);
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于关键文件检测项目类型
|
||||
*/
|
||||
private ProjectType detectByKeyFiles(Path projectRoot) throws IOException {
|
||||
// Java Maven项目
|
||||
if (Files.exists(projectRoot.resolve("pom.xml"))) {
|
||||
// 检查是否为Spring Boot项目
|
||||
if (isSpringBootProject(projectRoot)) {
|
||||
return ProjectType.SPRING_BOOT;
|
||||
}
|
||||
return ProjectType.JAVA_MAVEN;
|
||||
}
|
||||
|
||||
// Java Gradle项目
|
||||
if (Files.exists(projectRoot.resolve("build.gradle")) ||
|
||||
Files.exists(projectRoot.resolve("build.gradle.kts"))) {
|
||||
return ProjectType.JAVA_GRADLE;
|
||||
}
|
||||
|
||||
// Node.js项目
|
||||
if (Files.exists(projectRoot.resolve("package.json"))) {
|
||||
return analyzeNodeJsProject(projectRoot);
|
||||
}
|
||||
|
||||
// Python项目
|
||||
if (Files.exists(projectRoot.resolve("requirements.txt")) ||
|
||||
Files.exists(projectRoot.resolve("setup.py")) ||
|
||||
Files.exists(projectRoot.resolve("pyproject.toml"))) {
|
||||
return analyzePythonProject(projectRoot);
|
||||
}
|
||||
|
||||
// .NET项目
|
||||
try (Stream<Path> files = Files.list(projectRoot)) {
|
||||
if (files.anyMatch(path -> path.toString().endsWith(".csproj") ||
|
||||
path.toString().endsWith(".sln"))) {
|
||||
return ProjectType.DOTNET;
|
||||
}
|
||||
}
|
||||
|
||||
// Go项目
|
||||
if (Files.exists(projectRoot.resolve("go.mod"))) {
|
||||
return ProjectType.GO;
|
||||
}
|
||||
|
||||
// Rust项目
|
||||
if (Files.exists(projectRoot.resolve("Cargo.toml"))) {
|
||||
return ProjectType.RUST;
|
||||
}
|
||||
|
||||
// PHP项目
|
||||
if (Files.exists(projectRoot.resolve("composer.json"))) {
|
||||
return ProjectType.PHP;
|
||||
}
|
||||
|
||||
// 静态HTML项目
|
||||
if (Files.exists(projectRoot.resolve("index.html"))) {
|
||||
return ProjectType.HTML_STATIC;
|
||||
}
|
||||
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为Spring Boot项目
|
||||
*/
|
||||
private boolean isSpringBootProject(Path projectRoot) {
|
||||
try {
|
||||
Path pomFile = projectRoot.resolve("pom.xml");
|
||||
if (!Files.exists(pomFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String pomContent = Files.readString(pomFile);
|
||||
return pomContent.contains("spring-boot-starter") ||
|
||||
pomContent.contains("org.springframework.boot");
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading pom.xml for Spring Boot detection", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Node.js项目类型
|
||||
*/
|
||||
private ProjectType analyzeNodeJsProject(Path projectRoot) {
|
||||
try {
|
||||
Path packageJsonPath = projectRoot.resolve("package.json");
|
||||
String content = Files.readString(packageJsonPath);
|
||||
JsonNode packageJson = objectMapper.readTree(content);
|
||||
|
||||
// 检查依赖来确定具体的框架类型
|
||||
JsonNode dependencies = packageJson.get("dependencies");
|
||||
JsonNode devDependencies = packageJson.get("devDependencies");
|
||||
|
||||
if (hasDependency(dependencies, "react") || hasDependency(devDependencies, "react")) {
|
||||
return ProjectType.REACT;
|
||||
}
|
||||
|
||||
if (hasDependency(dependencies, "vue") || hasDependency(devDependencies, "vue")) {
|
||||
return ProjectType.VUE;
|
||||
}
|
||||
|
||||
if (hasDependency(dependencies, "@angular/core") ||
|
||||
hasDependency(devDependencies, "@angular/cli")) {
|
||||
return ProjectType.ANGULAR;
|
||||
}
|
||||
|
||||
if (hasDependency(dependencies, "next") || hasDependency(devDependencies, "next")) {
|
||||
return ProjectType.NEXT_JS;
|
||||
}
|
||||
|
||||
return ProjectType.NODE_JS;
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error analyzing package.json", e);
|
||||
return ProjectType.NODE_JS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析Python项目类型
|
||||
*/
|
||||
private ProjectType analyzePythonProject(Path projectRoot) {
|
||||
// 检查Django项目
|
||||
if (Files.exists(projectRoot.resolve("manage.py"))) {
|
||||
return ProjectType.DJANGO;
|
||||
}
|
||||
|
||||
// 检查Flask项目
|
||||
if (Files.exists(projectRoot.resolve("app.py")) ||
|
||||
Files.exists(projectRoot.resolve("application.py"))) {
|
||||
return ProjectType.FLASK;
|
||||
}
|
||||
|
||||
// 检查FastAPI项目
|
||||
if (Files.exists(projectRoot.resolve("main.py"))) {
|
||||
try {
|
||||
String content = Files.readString(projectRoot.resolve("main.py"));
|
||||
if (content.contains("from fastapi import") || content.contains("import fastapi")) {
|
||||
return ProjectType.FASTAPI;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading main.py for FastAPI detection", e);
|
||||
}
|
||||
}
|
||||
|
||||
return ProjectType.PYTHON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于目录结构检测项目类型
|
||||
*/
|
||||
private ProjectType detectByDirectoryStructure(Path projectRoot) {
|
||||
try {
|
||||
List<String> directories = Files.list(projectRoot)
|
||||
.filter(Files::isDirectory)
|
||||
.map(path -> path.getFileName().toString().toLowerCase())
|
||||
.toList();
|
||||
|
||||
// Java项目特征目录
|
||||
if (directories.contains("src") &&
|
||||
(directories.contains("target") || directories.contains("build"))) {
|
||||
return ProjectType.JAVA_MAVEN; // 默认为Maven
|
||||
}
|
||||
|
||||
// Node.js项目特征目录
|
||||
if (directories.contains("node_modules") ||
|
||||
directories.contains("public") ||
|
||||
directories.contains("dist")) {
|
||||
return ProjectType.NODE_JS;
|
||||
}
|
||||
|
||||
// Python项目特征目录
|
||||
if (directories.contains("venv") ||
|
||||
directories.contains("env") ||
|
||||
directories.contains("__pycache__")) {
|
||||
return ProjectType.PYTHON;
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error analyzing directory structure", e);
|
||||
}
|
||||
|
||||
return ProjectType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在特定依赖
|
||||
*/
|
||||
private boolean hasDependency(JsonNode dependencies, String dependencyName) {
|
||||
return dependencies != null && dependencies.has(dependencyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目类型的详细信息
|
||||
*/
|
||||
public String getProjectTypeDetails(Path projectRoot, ProjectType projectType) {
|
||||
StringBuilder details = new StringBuilder();
|
||||
details.append("Project Type: ").append(projectType.getDisplayName()).append("\n");
|
||||
details.append("Primary Language: ").append(projectType.getPrimaryLanguage()).append("\n");
|
||||
details.append("Package Manager: ").append(projectType.getPackageManager()).append("\n");
|
||||
|
||||
// 添加特定项目类型的详细信息
|
||||
switch (projectType) {
|
||||
case SPRING_BOOT:
|
||||
details.append("Framework: Spring Boot\n");
|
||||
details.append("Build Tool: Maven\n");
|
||||
break;
|
||||
case REACT:
|
||||
details.append("Framework: React\n");
|
||||
details.append("Runtime: Node.js\n");
|
||||
break;
|
||||
case DJANGO:
|
||||
details.append("Framework: Django\n");
|
||||
details.append("Language: Python\n");
|
||||
break;
|
||||
// 可以添加更多项目类型的详细信息
|
||||
}
|
||||
|
||||
return details.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class TaskSummaryService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TaskSummaryService.class);
|
||||
|
||||
private static final Pattern[] ACTION_PATTERNS = {
|
||||
Pattern.compile("(?i)creating?\\s+(?:a\\s+)?(?:new\\s+)?(.{1,50}?)(?:\\s+file|\\s+directory|\\s+project)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)writing?\\s+(?:to\\s+)?(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)reading?\\s+(?:from\\s+)?(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)editing?\\s+(.{1,50}?)(?:\\s+file)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)listing?\\s+(?:the\\s+)?(.{1,50}?)(?:\\s+directory)?", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)analyzing?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)generating?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("(?i)building?\\s+(.{1,50}?)", Pattern.CASE_INSENSITIVE)
|
||||
};
|
||||
|
||||
private static final String[] ACTION_VERBS = {
|
||||
"创建", "写入", "读取", "编辑", "列出", "分析", "生成", "构建",
|
||||
"creating", "writing", "reading", "editing", "listing", "analyzing", "generating", "building"
|
||||
};
|
||||
|
||||
/**
|
||||
* 从AI响应中提取任务摘要
|
||||
*/
|
||||
public String extractTaskSummary(String aiResponse) {
|
||||
if (aiResponse == null || aiResponse.trim().isEmpty()) {
|
||||
return "处理中...";
|
||||
}
|
||||
|
||||
// 清理响应文本
|
||||
String cleanResponse = aiResponse.replaceAll("```[\\s\\S]*?```", "").trim();
|
||||
|
||||
// 尝试匹配具体操作
|
||||
for (Pattern pattern : ACTION_PATTERNS) {
|
||||
Matcher matcher = pattern.matcher(cleanResponse);
|
||||
if (matcher.find()) {
|
||||
String action = matcher.group(0).trim();
|
||||
if (action.length() > 50) {
|
||||
action = action.substring(0, 47) + "...";
|
||||
}
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
// 查找动作词汇
|
||||
String lowerResponse = cleanResponse.toLowerCase();
|
||||
for (String verb : ACTION_VERBS) {
|
||||
if (lowerResponse.contains(verb.toLowerCase())) {
|
||||
// 提取包含动作词的句子
|
||||
String[] sentences = cleanResponse.split("[.!?\\n]");
|
||||
for (String sentence : sentences) {
|
||||
if (sentence.toLowerCase().contains(verb.toLowerCase())) {
|
||||
String summary = sentence.trim();
|
||||
if (summary.length() > 60) {
|
||||
summary = summary.substring(0, 57) + "...";
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到具体操作,返回通用描述
|
||||
if (cleanResponse.length() > 60) {
|
||||
return cleanResponse.substring(0, 57) + "...";
|
||||
}
|
||||
|
||||
return cleanResponse.isEmpty() ? "处理中..." : cleanResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算任务复杂度和预期轮数
|
||||
*/
|
||||
public int estimateTaskComplexity(String initialMessage) {
|
||||
if (initialMessage == null) return 1;
|
||||
|
||||
String lowerMessage = initialMessage.toLowerCase();
|
||||
int complexity = 1;
|
||||
|
||||
// 基于关键词估算复杂度
|
||||
if (lowerMessage.contains("project") || lowerMessage.contains("项目")) complexity += 3;
|
||||
if (lowerMessage.contains("complete") || lowerMessage.contains("完整")) complexity += 2;
|
||||
if (lowerMessage.contains("multiple") || lowerMessage.contains("多个")) complexity += 2;
|
||||
if (lowerMessage.contains("full-stack") || lowerMessage.contains("全栈")) complexity += 4;
|
||||
if (lowerMessage.contains("website") || lowerMessage.contains("网站")) complexity += 2;
|
||||
if (lowerMessage.contains("api") || lowerMessage.contains("接口")) complexity += 2;
|
||||
|
||||
// 基于文件操作数量估算
|
||||
long fileOperations = lowerMessage.chars()
|
||||
.mapToObj(c -> String.valueOf((char) c))
|
||||
.filter(s -> s.matches(".*(?:create|write|edit|file|directory).*"))
|
||||
.count();
|
||||
|
||||
complexity += (int) Math.min(fileOperations / 2, 5);
|
||||
|
||||
return Math.min(complexity, 15); // 最大15轮
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成当前状态的用户友好描述
|
||||
*/
|
||||
public String generateStatusDescription(String status, String currentAction, int currentTurn, int totalTurns) {
|
||||
StringBuilder desc = new StringBuilder();
|
||||
|
||||
switch (status) {
|
||||
case "RUNNING":
|
||||
if (currentAction != null && !currentAction.trim().isEmpty()) {
|
||||
desc.append("🔄 ").append(currentAction);
|
||||
} else {
|
||||
desc.append("🤔 AI正在思考...");
|
||||
}
|
||||
|
||||
if (totalTurns > 1) {
|
||||
desc.append(String.format(" (第%d/%d轮)", currentTurn, totalTurns));
|
||||
}
|
||||
break;
|
||||
|
||||
case "COMPLETED":
|
||||
desc.append("✅ 任务完成");
|
||||
if (totalTurns > 1) {
|
||||
desc.append(String.format(" (共%d轮)", currentTurn));
|
||||
}
|
||||
break;
|
||||
|
||||
case "ERROR":
|
||||
desc.append("❌ 执行出错");
|
||||
break;
|
||||
|
||||
default:
|
||||
desc.append("⏳ 处理中...");
|
||||
}
|
||||
|
||||
return desc.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 工具执行日志记录服务
|
||||
* 记录所有工具调用的详细信息,使用中文日志
|
||||
*/
|
||||
@Service
|
||||
public class ToolExecutionLogger {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ToolExecutionLogger.class);
|
||||
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
// 工具调用计数器
|
||||
private final AtomicLong callCounter = new AtomicLong(0);
|
||||
|
||||
// 工具执行统计
|
||||
private final Map<String, ToolStats> toolStats = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 记录工具调用开始
|
||||
*/
|
||||
public long logToolStart(String toolName, String description, Object parameters) {
|
||||
long callId = callCounter.incrementAndGet();
|
||||
String timestamp = LocalDateTime.now().format(formatter);
|
||||
|
||||
logger.info("🚀 [工具调用-{}] 开始执行工具: {}", callId, toolName);
|
||||
logger.info("📝 [工具调用-{}] 工具描述: {}", callId, description);
|
||||
logger.info("⚙️ [工具调用-{}] 调用参数: {}", callId, formatParameters(parameters));
|
||||
logger.info("🕐 [工具调用-{}] 开始时间: {}", callId, timestamp);
|
||||
|
||||
// 更新统计信息
|
||||
toolStats.computeIfAbsent(toolName, k -> new ToolStats()).incrementCalls();
|
||||
|
||||
return callId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具调用成功
|
||||
*/
|
||||
public void logToolSuccess(long callId, String toolName, String result, long executionTimeMs) {
|
||||
String timestamp = LocalDateTime.now().format(formatter);
|
||||
|
||||
logger.info("✅ [工具调用-{}] 工具执行成功: {}", callId, toolName);
|
||||
logger.info("📊 [工具调用-{}] 执行结果: {}", callId, truncateResult(result));
|
||||
logger.info("⏱️ [工具调用-{}] 执行耗时: {}ms", callId, executionTimeMs);
|
||||
logger.info("🕐 [工具调用-{}] 完成时间: {}", callId, timestamp);
|
||||
|
||||
// 更新统计信息
|
||||
ToolStats stats = toolStats.get(toolName);
|
||||
if (stats != null) {
|
||||
stats.incrementSuccess();
|
||||
stats.addExecutionTime(executionTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具调用失败
|
||||
*/
|
||||
public void logToolError(long callId, String toolName, String error, long executionTimeMs) {
|
||||
String timestamp = LocalDateTime.now().format(formatter);
|
||||
|
||||
logger.error("❌ [工具调用-{}] 工具执行失败: {}", callId, toolName);
|
||||
logger.error("🚨 [工具调用-{}] 错误信息: {}", callId, error);
|
||||
logger.error("⏱️ [工具调用-{}] 执行耗时: {}ms", callId, executionTimeMs);
|
||||
logger.error("🕐 [工具调用-{}] 失败时间: {}", callId, timestamp);
|
||||
|
||||
// 更新统计信息
|
||||
ToolStats stats = toolStats.get(toolName);
|
||||
if (stats != null) {
|
||||
stats.incrementError();
|
||||
stats.addExecutionTime(executionTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录工具调用的详细步骤
|
||||
*/
|
||||
public void logToolStep(long callId, String toolName, String step, String details) {
|
||||
logger.debug("🔄 [工具调用-{}] [{}] 执行步骤: {} - {}", callId, toolName, step, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录文件操作
|
||||
*/
|
||||
public void logFileOperation(long callId, String operation, String filePath, String details) {
|
||||
logger.info("📁 [工具调用-{}] 文件操作: {} - 文件: {} - 详情: {}", callId, operation, filePath, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录项目分析
|
||||
*/
|
||||
public void logProjectAnalysis(long callId, String projectPath, String projectType, String details) {
|
||||
logger.info("🔍 [工具调用-{}] 项目分析: 路径={}, 类型={}, 详情={}", callId, projectPath, projectType, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录项目创建
|
||||
*/
|
||||
public void logProjectCreation(long callId, String projectName, String projectType, String projectPath) {
|
||||
logger.info("🏗️ [工具调用-{}] 项目创建: 名称={}, 类型={}, 路径={}", callId, projectName, projectType, projectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具执行统计
|
||||
*/
|
||||
public void logToolStatistics() {
|
||||
logger.info("📈 ========== 工具执行统计 ==========");
|
||||
toolStats.forEach((toolName, stats) -> {
|
||||
logger.info("🔧 工具: {} | 调用次数: {} | 成功: {} | 失败: {} | 平均耗时: {}ms",
|
||||
toolName, stats.getTotalCalls(), stats.getSuccessCount(),
|
||||
stats.getErrorCount(), stats.getAverageExecutionTime());
|
||||
});
|
||||
logger.info("📈 ================================");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化参数显示
|
||||
*/
|
||||
private String formatParameters(Object parameters) {
|
||||
if (parameters == null) {
|
||||
return "无参数";
|
||||
}
|
||||
String paramStr = parameters.toString();
|
||||
return paramStr.length() > 200 ? paramStr.substring(0, 200) + "..." : paramStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断结果显示
|
||||
*/
|
||||
private String truncateResult(String result) {
|
||||
if (result == null) {
|
||||
return "无结果";
|
||||
}
|
||||
return result.length() > 300 ? result.substring(0, 300) + "..." : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具统计信息内部类
|
||||
*/
|
||||
private static class ToolStats {
|
||||
private long totalCalls = 0;
|
||||
private long successCount = 0;
|
||||
private long errorCount = 0;
|
||||
private long totalExecutionTime = 0;
|
||||
|
||||
public void incrementCalls() {
|
||||
totalCalls++;
|
||||
}
|
||||
|
||||
public void incrementSuccess() {
|
||||
successCount++;
|
||||
}
|
||||
|
||||
public void incrementError() {
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
public void addExecutionTime(long time) {
|
||||
totalExecutionTime += time;
|
||||
}
|
||||
|
||||
public long getTotalCalls() {
|
||||
return totalCalls;
|
||||
}
|
||||
|
||||
public long getSuccessCount() {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public long getErrorCount() {
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
public long getAverageExecutionTime() {
|
||||
return totalCalls > 0 ? totalExecutionTime / totalCalls : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
/**
|
||||
* 工具日志事件类
|
||||
* 继承自LogEvent,添加工具相关的字段
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ToolLogEvent extends LogEvent {
|
||||
|
||||
private String toolName;
|
||||
private String filePath;
|
||||
private String icon;
|
||||
private String status; // RUNNING, SUCCESS, ERROR
|
||||
private Long executionTime; // 执行时间(毫秒)
|
||||
private String summary; // 操作摘要
|
||||
|
||||
// Constructors
|
||||
public ToolLogEvent() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ToolLogEvent(String type, String taskId, String toolName, String filePath,
|
||||
String message, String timestamp, String icon, String status) {
|
||||
super(type, taskId, message, timestamp);
|
||||
this.toolName = toolName;
|
||||
this.filePath = filePath;
|
||||
this.icon = icon;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getToolName() {
|
||||
return toolName;
|
||||
}
|
||||
|
||||
public void setToolName(String toolName) {
|
||||
this.toolName = toolName;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(String icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getExecutionTime() {
|
||||
return executionTime;
|
||||
}
|
||||
|
||||
public void setExecutionTime(Long executionTime) {
|
||||
this.executionTime = executionTime;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ToolLogEvent{" +
|
||||
"toolName='" + toolName + '\'' +
|
||||
", filePath='" + filePath + '\'' +
|
||||
", icon='" + icon + '\'' +
|
||||
", status='" + status + '\'' +
|
||||
", executionTime=" + executionTime +
|
||||
", summary='" + summary + '\'' +
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.example.demo.utils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
/**
|
||||
* 浏览器工具类
|
||||
* 用于跨平台打开默认浏览器
|
||||
*/
|
||||
public class BrowserUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BrowserUtil.class);
|
||||
|
||||
/**
|
||||
* 打开默认浏览器访问指定URL
|
||||
*
|
||||
* @param url 要访问的URL
|
||||
* @return 是否成功打开
|
||||
*/
|
||||
public static boolean openBrowser(String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
logger.warn("URL is null or empty, cannot open browser");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 方法1: 使用Desktop API (推荐)
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
Desktop desktop = Desktop.getDesktop();
|
||||
if (desktop.isSupported(Desktop.Action.BROWSE)) {
|
||||
desktop.browse(new URI(url));
|
||||
logger.info("Successfully opened browser with URL: {}", url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 方法2: 使用系统命令 (备用方案)
|
||||
return openBrowserWithCommand(url);
|
||||
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
logger.error("Failed to open browser with Desktop API for URL: {}", url, e);
|
||||
// 尝试备用方案
|
||||
return openBrowserWithCommand(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用系统命令打开浏览器 (备用方案)
|
||||
*
|
||||
* @param url 要访问的URL
|
||||
* @return 是否成功打开
|
||||
*/
|
||||
private static boolean openBrowserWithCommand(String url) {
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
ProcessBuilder processBuilder;
|
||||
|
||||
if (os.contains("win")) {
|
||||
// Windows
|
||||
processBuilder = new ProcessBuilder("rundll32", "url.dll,FileProtocolHandler", url);
|
||||
} else if (os.contains("mac")) {
|
||||
// macOS
|
||||
processBuilder = new ProcessBuilder("open", url);
|
||||
} else {
|
||||
// Linux/Unix
|
||||
processBuilder = new ProcessBuilder("xdg-open", url);
|
||||
}
|
||||
|
||||
Process process = processBuilder.start();
|
||||
|
||||
// 等待一小段时间确保命令执行
|
||||
Thread.sleep(500);
|
||||
|
||||
logger.info("Successfully opened browser using system command for URL: {}", url);
|
||||
return true;
|
||||
|
||||
} catch (IOException | InterruptedException e) {
|
||||
logger.error("Failed to open browser using system command for URL: {}", url, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查URL是否有效
|
||||
*
|
||||
* @param url 要检查的URL
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValidUrl(String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
new URI(url);
|
||||
return url.startsWith("http://") || url.startsWith("https://");
|
||||
} catch (URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package com.example.demo.utils;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 跨平台路径处理工具类
|
||||
*/
|
||||
public class PathUtils {
|
||||
|
||||
/**
|
||||
* 构建跨平台兼容的绝对路径
|
||||
*
|
||||
* @param basePath 基础路径
|
||||
* @param relativePath 相对路径部分
|
||||
* @return 跨平台兼容的绝对路径
|
||||
*/
|
||||
public static String buildPath(String basePath, String... relativePath) {
|
||||
return Paths.get(basePath, relativePath).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径,确保跨平台兼容
|
||||
*
|
||||
* @param path 原始路径
|
||||
* @return 规范化后的路径
|
||||
*/
|
||||
public static String normalizePath(String path) {
|
||||
return Paths.get(path).normalize().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否为绝对路径
|
||||
*
|
||||
* @param path 要检查的路径
|
||||
* @return 是否为绝对路径
|
||||
*/
|
||||
public static boolean isAbsolute(String path) {
|
||||
return Paths.get(path).isAbsolute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前工作目录
|
||||
*
|
||||
* @return 当前工作目录的绝对路径
|
||||
*/
|
||||
public static String getCurrentWorkingDirectory() {
|
||||
return System.getProperty("user.dir");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工作空间路径
|
||||
*
|
||||
* @return 工作空间的绝对路径
|
||||
*/
|
||||
public static String buildWorkspacePath() {
|
||||
return buildPath(getCurrentWorkingDirectory(), "workspace");
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,31 +0,0 @@
|
||||
# ai-tutor-skill
|
||||
|
||||
A Claude skill for explaining complex AI and ML concepts in accessible, plain English. This skill transforms abstract technical ideas into clear explanations using structured narrative frameworks, making it ideal for teaching and learning technical topics.
|
||||
|
||||
Resources:
|
||||
- [YouTube Explainer](https://youtu.be/vEvytl7wrGM)
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Python**: >= 3.12
|
||||
- **Package Manager**: [uv](https://github.com/astral-sh/uv)
|
||||
- **Dependencies**: youtube-transcript-api (installed automatically)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd ai-tutor-skill
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
## Important Note: YouTube Transcript Limitations
|
||||
|
||||
> **The YouTube transcript functionality only works when Claude Code is running locally.**
|
||||
>
|
||||
> YouTube blocks requests from Claude's servers, so transcript extraction will fail when using Claude Code in cloud/remote mode. To use this feature, ensure you're running Claude Code on your local machine.
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
name: ai-tutor
|
||||
description: Use when user asks to explain, break down, or help understand technical concepts (AI, ML, or other technical topics). Makes complex ideas accessible through plain English and narrative structure. Use the provided scripts to transcribe videos
|
||||
---
|
||||
|
||||
# AI Tutor
|
||||
|
||||
Transform complex technical concepts into clear, accessible explanations using narrative storytelling frameworks.
|
||||
|
||||
## Before Responding: Think Hard
|
||||
|
||||
Before crafting your explanation:
|
||||
|
||||
1. **Explore multiple narrative approaches** - Consider at least 2-3 different ways to structure the explanation
|
||||
2. **Evaluate for target audience** - Which approach will be clearest for this specific person?
|
||||
3. **Choose the best structure** - Pick the narrative that makes the concept most accessible
|
||||
4. **Plan your examples** - Identify concrete, specific examples before writing
|
||||
|
||||
Take time to think through these options. A well-chosen structure is more valuable than a quick response.
|
||||
|
||||
**If concept is unfamiliar or requires research:** Load `research_methodology.md` for detailed guidance.
|
||||
**If user provides YouTube video:** Call `uv run scripts/get_youtube_transcript.py <video_url_or_id>` for video's transcript.
|
||||
|
||||
## Core Teaching Framework
|
||||
|
||||
Use one of three narrative structures:
|
||||
|
||||
### Status Quo → Problem → Solution
|
||||
1. **Status Quo**: Describe the existing situation or baseline approach
|
||||
2. **Problem**: Explain what's broken, inefficient, or limiting
|
||||
3. **Solution**: Show how the concept solves the problem
|
||||
|
||||
This is the primary go-to structure.
|
||||
|
||||
### What → Why → How
|
||||
1. **What**: Define the concept in simple terms (what it is)
|
||||
2. **Why**: Explain the motivation and importance (why it matters)
|
||||
3. **How**: Break down the mechanics (how it works)
|
||||
|
||||
### What → So What → What Now
|
||||
1. **What**: State the situation or finding
|
||||
2. **So What**: Explain the implications or impact
|
||||
3. **What Now**: Describe next steps or actions
|
||||
|
||||
Use for business contexts and practical applications.
|
||||
|
||||
## Teaching Principles
|
||||
|
||||
### Plain English First
|
||||
Replace technical jargon with clear, direct explanations of the core concept.
|
||||
|
||||
**Example:**
|
||||
- ❌ "The gradient descent algorithm optimizes the loss function via backpropagation"
|
||||
- ✅ "Gradient descent is a way to find the model parameters that make the best predictions based on real-world data"
|
||||
|
||||
Plain English means explaining the concept directly without jargon—not just using analogies.
|
||||
|
||||
### Concrete Examples Ground Abstract Ideas
|
||||
Always provide at least one concrete example with specific details, numbers, or real instances.
|
||||
|
||||
**Example:**
|
||||
- Abstract: "Features are things we use to make predictions"
|
||||
- Concrete: "For our customer churn model, features include age of account and number of logins in the past 90 days"
|
||||
|
||||
### Use Analogies Judiciously
|
||||
Analogies map the unfamiliar to the familiar, but use them sparingly and strategically—not as the primary explanation method.
|
||||
|
||||
**When to use:**
|
||||
- After explaining the concept in plain English
|
||||
- When the technical concept has a strong parallel to everyday experience
|
||||
- To create memorable mental models
|
||||
|
||||
Avoid over-relying on analogies. Start with direct, plain English explanations.
|
||||
|
||||
### Progressive Complexity
|
||||
- Start with the intuition and big picture
|
||||
- Add details layer by layer
|
||||
- Use concrete examples before abstractions
|
||||
- Build from familiar to unfamiliar
|
||||
|
||||
### Less is More
|
||||
Attention and mental effort are finite. Be economical with your audience's cognitive resources.
|
||||
- Cut unnecessary fluff
|
||||
- Every word should earn its place
|
||||
- Focus attention on key information
|
||||
|
||||
### Use Numbered Lists Strategically
|
||||
Numbers help navigate information and make it more digestible (e.g., "3 ways to fine-tune", "System 1 and System 2").
|
||||
|
||||
### Know Thy Audience
|
||||
Adjust technical depth, terminology, and focus based on who you're talking to.
|
||||
|
||||
**C-Suite / Business Leaders:**
|
||||
- Use high-level terms (e.g., "AI")
|
||||
- Focus on what and why, emphasize business impact
|
||||
- Keep it high-level, skip implementation details
|
||||
|
||||
**BI Analysts / Technical Adjacent:**
|
||||
- Use more specific terms (e.g., "LLM")
|
||||
- Cover what and why with more technical context
|
||||
- Discuss workflow relevance, include moderate technical details
|
||||
|
||||
**Data Scientists / Technical Peers:**
|
||||
- Use precise terminology (e.g., "Llama 3 8B")
|
||||
- Cover what, why, AND how
|
||||
- Dive into technical details, discuss specific implementation
|
||||
- Still emphasize business impact (everyone wants to know why)
|
||||
|
||||
**If audience level is unclear:** Assume the lowest level of understanding and explain accordingly. Don't ask the user to clarify—just start with fundamentals. You can always go deeper if they ask for more detail.
|
||||
|
||||
## Response Style
|
||||
|
||||
- Start with the big picture before diving into details
|
||||
- Use conversational, friendly tone
|
||||
- Offer to explain subsections in more depth
|
||||
- Use bullet points sparingly—prefer flowing narrative prose
|
||||
- Include concrete examples with specific details
|
||||
- Connect concepts to real-world applications
|
||||
- Be economical with words—every sentence should add value
|
||||
|
||||
## Workflow Summary
|
||||
|
||||
1. **Think hard**: Explore 2-3 narrative structures, choose the clearest for the audience
|
||||
2. **Identify audience**: Assess knowledge level (if unclear, assume beginner level)
|
||||
3. **Check if research needed**:
|
||||
- Can you explain this with your existing knowledge? → Proceed to step 4
|
||||
- Unfamiliar/cutting-edge topic? → Load `research_methodology.md` first
|
||||
4. **Craft explanation**: Plain English first, no jargon
|
||||
5. **Add concrete example**: Specific details, numbers, real instances
|
||||
6. **Optional analogy**: Only if it adds value beyond direct explanation
|
||||
7. **Offer to dive deeper**: Invite questions on specific aspects
|
||||
@@ -1,9 +0,0 @@
|
||||
[project]
|
||||
name = "ai-tutor"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"youtube-transcript-api>=1.2.3",
|
||||
]
|
||||
@@ -1,218 +0,0 @@
|
||||
# Research Methodology for Explaining Unfamiliar Concepts
|
||||
|
||||
Use this guide when you encounter concepts outside your reliable knowledge or when explaining cutting-edge developments.
|
||||
|
||||
## When to Research
|
||||
|
||||
**Always research when:**
|
||||
- Concept is unfamiliar or outside your training data
|
||||
- Topic involves developments after early 2025
|
||||
- User references specific papers, articles, or sources
|
||||
- Explaining cutting-edge techniques or recent breakthroughs
|
||||
- You're uncertain about technical accuracy
|
||||
- User asks "what's new" or "recent developments"
|
||||
|
||||
**Don't research when:**
|
||||
- Explaining well-established, fundamental concepts (e.g., gradient descent, neural networks)
|
||||
- You have high confidence in your knowledge
|
||||
- Topic is clearly pre-2025 and stable
|
||||
|
||||
## Research Strategy
|
||||
|
||||
### 1. Start with Broad Context (web_search)
|
||||
|
||||
**Effective search queries:**
|
||||
- For new concepts: `"{concept name}" explained tutorial`
|
||||
- For recent developments: `"{concept}" 2024 2025 latest`
|
||||
- For comparisons: `"{concept A}" vs "{concept B}" differences`
|
||||
- For practical use: `"{concept}" real world applications examples`
|
||||
|
||||
**Evaluate search results:**
|
||||
- Prioritize: Official documentation, academic institutions, reputable tech blogs
|
||||
- Look for: Recent dates, author credentials, technical depth
|
||||
- Avoid: Marketing content, SEO spam, unsourced claims
|
||||
|
||||
**Extract from results:**
|
||||
- Core definition in plain language
|
||||
- Key motivations (what problem it solves)
|
||||
- Main components or mechanisms
|
||||
- Concrete examples or applications
|
||||
- Common misconceptions
|
||||
|
||||
### 2. Deep Dive on Best Sources (web_fetch)
|
||||
|
||||
**When to fetch full content:**
|
||||
- Found a particularly clear explanation
|
||||
- Need technical details for accuracy
|
||||
- Source is academic paper or official documentation
|
||||
- Initial search didn't provide sufficient depth
|
||||
|
||||
**What to extract from full articles:**
|
||||
- The author's own plain English summary (often in intro/conclusion)
|
||||
- Concrete examples with specific numbers or data
|
||||
- Diagrams or visual explanations (note what they show)
|
||||
- Comparison to previous/alternative approaches
|
||||
- Practical applications or use cases
|
||||
|
||||
**Reading academic papers:**
|
||||
- Start with abstract and conclusion
|
||||
- Look for "In this paper, we..." statements for plain English summary
|
||||
- Check "Related Work" section to understand context
|
||||
- Extract key innovation/contribution in one sentence
|
||||
- Find any "intuition" or "motivation" sections
|
||||
|
||||
### 3. Synthesize Multiple Sources
|
||||
|
||||
**When sources agree:**
|
||||
- Use the clearest explanation as your base
|
||||
- Incorporate best concrete examples from various sources
|
||||
- Combine different perspectives for completeness
|
||||
|
||||
**When sources conflict:**
|
||||
- Identify what they disagree about
|
||||
- Look for authoritative sources (original papers, official docs)
|
||||
- Note the disagreement in your explanation if significant
|
||||
- Don't hide uncertainty - acknowledge different perspectives
|
||||
|
||||
**Red flags to watch for:**
|
||||
- Single source makes claims not found elsewhere
|
||||
- Marketing language disguised as technical explanation
|
||||
- Overly simplified analogies that mislead
|
||||
- Cherry-picked benchmarks or examples
|
||||
|
||||
### 4. Extract from YouTube Videos
|
||||
|
||||
**When to use YouTube transcripts:**
|
||||
- User directly references a video
|
||||
- Video is from reputable educator/researcher
|
||||
- Need concrete examples from tutorial content
|
||||
- Want to see how concept is explained to learners
|
||||
|
||||
**Extracting from transcripts:**
|
||||
```bash
|
||||
# Basic usage - returns full transcript
|
||||
uv run scripts/get_youtube_transcript.py <video_url_or_id>
|
||||
|
||||
# With timestamps for reference
|
||||
uv run scripts/get_youtube_transcript.py <video_url_or_id> --timestamps
|
||||
|
||||
# Supports multiple URL formats
|
||||
uv run scripts/get_youtube_transcript.py dQw4w9WgXcQ
|
||||
uv run scripts/get_youtube_transcript.py https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
||||
uv run scripts/get_youtube_transcript.py https://youtu.be/dQw4w9WgXcQ
|
||||
```
|
||||
|
||||
**What to look for in transcripts:**
|
||||
- The educator's own analogies (often well-tested)
|
||||
- Concrete examples with walkthroughs
|
||||
- Common student questions addressed
|
||||
- Simpler explanations before technical ones
|
||||
- Visual descriptions ("as you can see in this diagram...")
|
||||
|
||||
**Transcript limitations:**
|
||||
- May include verbal fillers and repetition
|
||||
- Missing visual context (slides, diagrams)
|
||||
- Informal language may need translation to written form
|
||||
|
||||
## Source Quality Hierarchy
|
||||
|
||||
**Tier 1 (Highest Trust):**
|
||||
- Original research papers from reputable venues
|
||||
- Official documentation from source (e.g., OpenAI docs for GPT)
|
||||
- University course materials
|
||||
- Books from established publishers
|
||||
|
||||
**Tier 2 (High Trust):**
|
||||
- Technical blogs from recognized experts
|
||||
- Conference presentations and talks
|
||||
- Reputable tech news sites (with technical depth)
|
||||
- Well-maintained wikis with citations
|
||||
|
||||
**Tier 3 (Use with Caution):**
|
||||
- Medium articles (verify author credentials)
|
||||
- Stack Overflow (good for practical issues, not concepts)
|
||||
- Reddit discussions (good for perspectives, not authority)
|
||||
- Tutorial sites (verify accuracy against Tier 1/2 sources)
|
||||
|
||||
**Tier 4 (Avoid):**
|
||||
- Marketing materials posing as education
|
||||
- Uncited claims
|
||||
- Sensationalized headlines
|
||||
- Anonymous sources without verifiable expertise
|
||||
|
||||
## Handling Uncertainty
|
||||
|
||||
**When research reveals gaps:**
|
||||
- Be explicit: "Based on the sources I found..."
|
||||
- Explain what you learned and what remains unclear
|
||||
- Offer to research specific aspects more deeply
|
||||
- Don't fill gaps with speculation
|
||||
|
||||
**When sources are insufficient:**
|
||||
- State what you know with confidence
|
||||
- Acknowledge limitations: "The available sources don't provide clear information on..."
|
||||
- Suggest where user might find more detail
|
||||
- Offer to continue researching if user wants
|
||||
|
||||
**When completely unfamiliar:**
|
||||
- Don't hide it: "This is a cutting-edge concept I need to research"
|
||||
- Do thorough research before explaining
|
||||
- Synthesize from multiple high-quality sources
|
||||
- Be clear about confidence level in your explanation
|
||||
|
||||
## Common Research Mistakes to Avoid
|
||||
|
||||
❌ **Relying on single source** - Always cross-reference
|
||||
❌ **Using first search result** - Evaluate multiple sources
|
||||
❌ **Ignoring publication date** - Recent developments need recent sources
|
||||
❌ **Accepting marketing claims** - Verify with technical sources
|
||||
❌ **Skipping paper abstracts** - Authors' own summaries are gold
|
||||
❌ **Over-trusting tutorials** - Verify technical accuracy
|
||||
❌ **Hiding uncertainty** - Better to acknowledge gaps
|
||||
|
||||
## Research Workflow Example
|
||||
|
||||
**User asks:** "Explain mixture of experts in LLMs"
|
||||
|
||||
**Step 1 - Quick assessment:**
|
||||
- Topic: Recent development (2023-2024)
|
||||
- Confidence: Medium (know concept but not latest implementations)
|
||||
- Decision: Research needed
|
||||
|
||||
**Step 2 - Broad search:**
|
||||
```
|
||||
web_search: "mixture of experts LLMs 2024 explained"
|
||||
```
|
||||
- Find: Mixtral announcement, technical blog posts, comparisons
|
||||
- Note: Different from traditional MoE in NLP
|
||||
- Extract: Core idea, recent models using it, key benefits
|
||||
|
||||
**Step 3 - Deep dive:**
|
||||
```
|
||||
web_fetch: [Best technical blog or paper URL]
|
||||
```
|
||||
- Extract: Technical details, architecture specifics
|
||||
- Find: Concrete comparison (Mixtral 8x7B vs GPT-3.5)
|
||||
- Note: Load balancing, routing mechanisms
|
||||
|
||||
**Step 4 - Synthesize:**
|
||||
- Core concept: Sparse activation of expert networks
|
||||
- Problem it solves: Scaling without proportional compute increase
|
||||
- How it works: Router selects subset of experts per token
|
||||
- Example: Mixtral uses 8 experts, activates 2 per token
|
||||
- Result: 47B parameters, 13B active per token
|
||||
|
||||
**Step 5 - Explain:**
|
||||
Use Status Quo → Problem → Solution structure with researched content.
|
||||
|
||||
## Integration with Teaching Principles
|
||||
|
||||
After research, apply teaching framework:
|
||||
|
||||
1. **Choose narrative structure** based on concept nature
|
||||
2. **Plain English first** - use clearest definition found
|
||||
3. **Concrete examples** - use specific instances from research
|
||||
4. **Strategic analogies** - adopt effective ones from sources
|
||||
5. **Cite implicitly** - "Recent research shows..." not "According to source X..."
|
||||
|
||||
Research informs content; teaching principles guide delivery.
|
||||
@@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
YouTube Transcript Extractor
|
||||
|
||||
Extracts transcripts from YouTube videos using video IDs or URLs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
from youtube_transcript_api import YouTubeTranscriptApi
|
||||
from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound
|
||||
|
||||
|
||||
def extract_video_id(url_or_id):
|
||||
"""
|
||||
Extract YouTube video ID from various URL formats or return as-is if already an ID.
|
||||
|
||||
Supports:
|
||||
- https://www.youtube.com/watch?v=VIDEO_ID
|
||||
- https://youtu.be/VIDEO_ID
|
||||
- VIDEO_ID (direct ID)
|
||||
"""
|
||||
# Pattern for YouTube URLs
|
||||
patterns = [
|
||||
r'(?:youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})',
|
||||
r'(?:youtu\.be\/)([a-zA-Z0-9_-]{11})',
|
||||
r'^([a-zA-Z0-9_-]{11})$' # Direct video ID
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, url_or_id)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_transcript(video_id, language='en'):
|
||||
"""
|
||||
Retrieve transcript for a YouTube video.
|
||||
|
||||
Args:
|
||||
video_id: YouTube video ID
|
||||
language: Language code (default: 'en')
|
||||
|
||||
Returns:
|
||||
List of transcript entries with 'text', 'start', and 'duration'
|
||||
"""
|
||||
try:
|
||||
api = YouTubeTranscriptApi()
|
||||
transcript = api.fetch(video_id, languages=[language])
|
||||
return transcript
|
||||
except TranscriptsDisabled:
|
||||
raise Exception(f"Transcripts are disabled for video: {video_id}")
|
||||
except NoTranscriptFound:
|
||||
raise Exception(f"No transcript found for video: {video_id}")
|
||||
except Exception as e:
|
||||
raise Exception(f"Error fetching transcript: {str(e)}")
|
||||
|
||||
|
||||
def format_transcript(transcript, include_timestamps=False):
|
||||
"""
|
||||
Format transcript entries into readable text.
|
||||
|
||||
Args:
|
||||
transcript: List of transcript entries
|
||||
include_timestamps: Whether to include timestamps
|
||||
|
||||
Returns:
|
||||
Formatted transcript text
|
||||
"""
|
||||
if include_timestamps:
|
||||
formatted = []
|
||||
for entry in transcript:
|
||||
minutes = int(entry.start // 60)
|
||||
seconds = int(entry.start % 60)
|
||||
formatted.append(f"[{minutes}:{seconds:02d}] {entry.text}")
|
||||
return '\n'.join(formatted)
|
||||
else:
|
||||
return ' '.join([entry.text for entry in transcript])
|
||||
|
||||
|
||||
def main():
|
||||
"""Main execution function"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python get_youtube_transcript.py <video_url_or_id> [--timestamps]")
|
||||
print("\nExamples:")
|
||||
print(" uv run get_youtube_transcript.py dQw4w9WgXcQ")
|
||||
print(" uv run get_youtube_transcript.py https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||
print(" uv run get_youtube_transcript.py dQw4w9WgXcQ --timestamps")
|
||||
sys.exit(1)
|
||||
|
||||
url_or_id = sys.argv[1]
|
||||
include_timestamps = '--timestamps' in sys.argv
|
||||
|
||||
# Extract video ID
|
||||
video_id = extract_video_id(url_or_id)
|
||||
if not video_id:
|
||||
print(f"Error: Could not extract video ID from: {url_or_id}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
# Get transcript
|
||||
transcript = get_transcript(video_id)
|
||||
|
||||
# Format and print
|
||||
formatted_text = format_transcript(transcript, include_timestamps)
|
||||
print(formatted_text)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,135 +0,0 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "ai-tutor"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "youtube-transcript-api" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "youtube-transcript-api", specifier = ">=1.2.3" }]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "youtube-transcript-api"
|
||||
version = "1.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "defusedxml" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/03/68c69b2d3e282d45cb3c07e5836a9146ff9574cde720570ffc7eb124e56b/youtube_transcript_api-1.2.3.tar.gz", hash = "sha256:76016b71b410b124892c74df24b07b052702cf3c53afb300d0a2c547c0b71b68", size = 469757, upload-time = "2025-10-13T15:57:17.532Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/75/a861661b73d862e323c12af96ecfb237fb4d1433e551183d4172d39d5275/youtube_transcript_api-1.2.3-py3-none-any.whl", hash = "sha256:0c1b32ea5e739f9efde8c42e3d43e67df475185af6f820109607577b83768375", size = 485140, upload-time = "2025-10-13T15:57:16.034Z" },
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
© 2025 Anthropic, PBC. All rights reserved.
|
||||
|
||||
LICENSE: Use of these materials (including all code, prompts, assets, files,
|
||||
and other components of this Skill) is governed by your agreement with
|
||||
Anthropic regarding use of Anthropic's services. If no separate agreement
|
||||
exists, use is governed by Anthropic's Consumer Terms of Service or
|
||||
Commercial Terms of Service, as applicable:
|
||||
https://www.anthropic.com/legal/consumer-terms
|
||||
https://www.anthropic.com/legal/commercial-terms
|
||||
Your applicable agreement is referred to as the "Agreement." "Services" are
|
||||
as defined in the Agreement.
|
||||
|
||||
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
|
||||
contrary, users may not:
|
||||
|
||||
- Extract these materials from the Services or retain copies of these
|
||||
materials outside the Services
|
||||
- Reproduce or copy these materials, except for temporary copies created
|
||||
automatically during authorized use of the Services
|
||||
- Create derivative works based on these materials
|
||||
- Distribute, sublicense, or transfer these materials to any third party
|
||||
- Make, offer to sell, sell, or import any inventions embodied in these
|
||||
materials
|
||||
- Reverse engineer, decompile, or disassemble these materials
|
||||
|
||||
The receipt, viewing, or possession of these materials does not convey or
|
||||
imply any license or right beyond those expressly granted above.
|
||||
|
||||
Anthropic retains all right, title, and interest in these materials,
|
||||
including all copyrights, patents, and other intellectual property rights.
|
||||
@@ -1,294 +0,0 @@
|
||||
---
|
||||
name: pdf
|
||||
description: Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.
|
||||
license: Proprietary. LICENSE.txt has complete terms
|
||||
---
|
||||
|
||||
# PDF Processing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see reference.md. If you need to fill out a PDF form, read forms.md and follow its instructions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
# Read a PDF
|
||||
reader = PdfReader("document.pdf")
|
||||
print(f"Pages: {len(reader.pages)}")
|
||||
|
||||
# Extract text
|
||||
text = ""
|
||||
for page in reader.pages:
|
||||
text += page.extract_text()
|
||||
```
|
||||
|
||||
## Python Libraries
|
||||
|
||||
### pypdf - Basic Operations
|
||||
|
||||
#### Merge PDFs
|
||||
```python
|
||||
from pypdf import PdfWriter, PdfReader
|
||||
|
||||
writer = PdfWriter()
|
||||
for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]:
|
||||
reader = PdfReader(pdf_file)
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
|
||||
with open("merged.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
#### Split PDF
|
||||
```python
|
||||
reader = PdfReader("input.pdf")
|
||||
for i, page in enumerate(reader.pages):
|
||||
writer = PdfWriter()
|
||||
writer.add_page(page)
|
||||
with open(f"page_{i+1}.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
#### Extract Metadata
|
||||
```python
|
||||
reader = PdfReader("document.pdf")
|
||||
meta = reader.metadata
|
||||
print(f"Title: {meta.title}")
|
||||
print(f"Author: {meta.author}")
|
||||
print(f"Subject: {meta.subject}")
|
||||
print(f"Creator: {meta.creator}")
|
||||
```
|
||||
|
||||
#### Rotate Pages
|
||||
```python
|
||||
reader = PdfReader("input.pdf")
|
||||
writer = PdfWriter()
|
||||
|
||||
page = reader.pages[0]
|
||||
page.rotate(90) # Rotate 90 degrees clockwise
|
||||
writer.add_page(page)
|
||||
|
||||
with open("rotated.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
### pdfplumber - Text and Table Extraction
|
||||
|
||||
#### Extract Text with Layout
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
for page in pdf.pages:
|
||||
text = page.extract_text()
|
||||
print(text)
|
||||
```
|
||||
|
||||
#### Extract Tables
|
||||
```python
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
for i, page in enumerate(pdf.pages):
|
||||
tables = page.extract_tables()
|
||||
for j, table in enumerate(tables):
|
||||
print(f"Table {j+1} on page {i+1}:")
|
||||
for row in table:
|
||||
print(row)
|
||||
```
|
||||
|
||||
#### Advanced Table Extraction
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
all_tables = []
|
||||
for page in pdf.pages:
|
||||
tables = page.extract_tables()
|
||||
for table in tables:
|
||||
if table: # Check if table is not empty
|
||||
df = pd.DataFrame(table[1:], columns=table[0])
|
||||
all_tables.append(df)
|
||||
|
||||
# Combine all tables
|
||||
if all_tables:
|
||||
combined_df = pd.concat(all_tables, ignore_index=True)
|
||||
combined_df.to_excel("extracted_tables.xlsx", index=False)
|
||||
```
|
||||
|
||||
### reportlab - Create PDFs
|
||||
|
||||
#### Basic PDF Creation
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas("hello.pdf", pagesize=letter)
|
||||
width, height = letter
|
||||
|
||||
# Add text
|
||||
c.drawString(100, height - 100, "Hello World!")
|
||||
c.drawString(100, height - 120, "This is a PDF created with reportlab")
|
||||
|
||||
# Add a line
|
||||
c.line(100, height - 140, 400, height - 140)
|
||||
|
||||
# Save
|
||||
c.save()
|
||||
```
|
||||
|
||||
#### Create PDF with Multiple Pages
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
|
||||
doc = SimpleDocTemplate("report.pdf", pagesize=letter)
|
||||
styles = getSampleStyleSheet()
|
||||
story = []
|
||||
|
||||
# Add content
|
||||
title = Paragraph("Report Title", styles['Title'])
|
||||
story.append(title)
|
||||
story.append(Spacer(1, 12))
|
||||
|
||||
body = Paragraph("This is the body of the report. " * 20, styles['Normal'])
|
||||
story.append(body)
|
||||
story.append(PageBreak())
|
||||
|
||||
# Page 2
|
||||
story.append(Paragraph("Page 2", styles['Heading1']))
|
||||
story.append(Paragraph("Content for page 2", styles['Normal']))
|
||||
|
||||
# Build PDF
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
## Command-Line Tools
|
||||
|
||||
### pdftotext (poppler-utils)
|
||||
```bash
|
||||
# Extract text
|
||||
pdftotext input.pdf output.txt
|
||||
|
||||
# Extract text preserving layout
|
||||
pdftotext -layout input.pdf output.txt
|
||||
|
||||
# Extract specific pages
|
||||
pdftotext -f 1 -l 5 input.pdf output.txt # Pages 1-5
|
||||
```
|
||||
|
||||
### qpdf
|
||||
```bash
|
||||
# Merge PDFs
|
||||
qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf
|
||||
|
||||
# Split pages
|
||||
qpdf input.pdf --pages . 1-5 -- pages1-5.pdf
|
||||
qpdf input.pdf --pages . 6-10 -- pages6-10.pdf
|
||||
|
||||
# Rotate pages
|
||||
qpdf input.pdf output.pdf --rotate=+90:1 # Rotate page 1 by 90 degrees
|
||||
|
||||
# Remove password
|
||||
qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf
|
||||
```
|
||||
|
||||
### pdftk (if available)
|
||||
```bash
|
||||
# Merge
|
||||
pdftk file1.pdf file2.pdf cat output merged.pdf
|
||||
|
||||
# Split
|
||||
pdftk input.pdf burst
|
||||
|
||||
# Rotate
|
||||
pdftk input.pdf rotate 1east output rotated.pdf
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Extract Text from Scanned PDFs
|
||||
```python
|
||||
# Requires: pip install pytesseract pdf2image
|
||||
import pytesseract
|
||||
from pdf2image import convert_from_path
|
||||
|
||||
# Convert PDF to images
|
||||
images = convert_from_path('scanned.pdf')
|
||||
|
||||
# OCR each page
|
||||
text = ""
|
||||
for i, image in enumerate(images):
|
||||
text += f"Page {i+1}:\n"
|
||||
text += pytesseract.image_to_string(image)
|
||||
text += "\n\n"
|
||||
|
||||
print(text)
|
||||
```
|
||||
|
||||
### Add Watermark
|
||||
```python
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
# Create watermark (or load existing)
|
||||
watermark = PdfReader("watermark.pdf").pages[0]
|
||||
|
||||
# Apply to all pages
|
||||
reader = PdfReader("document.pdf")
|
||||
writer = PdfWriter()
|
||||
|
||||
for page in reader.pages:
|
||||
page.merge_page(watermark)
|
||||
writer.add_page(page)
|
||||
|
||||
with open("watermarked.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
### Extract Images
|
||||
```bash
|
||||
# Using pdfimages (poppler-utils)
|
||||
pdfimages -j input.pdf output_prefix
|
||||
|
||||
# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc.
|
||||
```
|
||||
|
||||
### Password Protection
|
||||
```python
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
reader = PdfReader("input.pdf")
|
||||
writer = PdfWriter()
|
||||
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
|
||||
# Add password
|
||||
writer.encrypt("userpassword", "ownerpassword")
|
||||
|
||||
with open("encrypted.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Best Tool | Command/Code |
|
||||
|------|-----------|--------------|
|
||||
| Merge PDFs | pypdf | `writer.add_page(page)` |
|
||||
| Split PDFs | pypdf | One page per file |
|
||||
| Extract text | pdfplumber | `page.extract_text()` |
|
||||
| Extract tables | pdfplumber | `page.extract_tables()` |
|
||||
| Create PDFs | reportlab | Canvas or Platypus |
|
||||
| Command line merge | qpdf | `qpdf --empty --pages ...` |
|
||||
| OCR scanned PDFs | pytesseract | Convert to image first |
|
||||
| Fill PDF forms | pdf-lib or pypdf (see forms.md) | See forms.md |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- For advanced pypdfium2 usage, see reference.md
|
||||
- For JavaScript libraries (pdf-lib), see reference.md
|
||||
- If you need to fill out a PDF form, follow the instructions in forms.md
|
||||
- For troubleshooting guides, see reference.md
|
||||
@@ -1,205 +0,0 @@
|
||||
**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.**
|
||||
|
||||
If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory:
|
||||
`python scripts/check_fillable_fields <file.pdf>`, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions.
|
||||
|
||||
# Fillable fields
|
||||
If the PDF has fillable form fields:
|
||||
- Run this script from this file's directory: `python scripts/extract_form_field_info.py <input.pdf> <field_info.json>`. It will create a JSON file with a list of fields in this format:
|
||||
```
|
||||
[
|
||||
{
|
||||
"field_id": (unique ID for the field),
|
||||
"page": (page number, 1-based),
|
||||
"rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page),
|
||||
"type": ("text", "checkbox", "radio_group", or "choice"),
|
||||
},
|
||||
// Checkboxes have "checked_value" and "unchecked_value" properties:
|
||||
{
|
||||
"field_id": (unique ID for the field),
|
||||
"page": (page number, 1-based),
|
||||
"type": "checkbox",
|
||||
"checked_value": (Set the field to this value to check the checkbox),
|
||||
"unchecked_value": (Set the field to this value to uncheck the checkbox),
|
||||
},
|
||||
// Radio groups have a "radio_options" list with the possible choices.
|
||||
{
|
||||
"field_id": (unique ID for the field),
|
||||
"page": (page number, 1-based),
|
||||
"type": "radio_group",
|
||||
"radio_options": [
|
||||
{
|
||||
"value": (set the field to this value to select this radio option),
|
||||
"rect": (bounding box for the radio button for this option)
|
||||
},
|
||||
// Other radio options
|
||||
]
|
||||
},
|
||||
// Multiple choice fields have a "choice_options" list with the possible choices:
|
||||
{
|
||||
"field_id": (unique ID for the field),
|
||||
"page": (page number, 1-based),
|
||||
"type": "choice",
|
||||
"choice_options": [
|
||||
{
|
||||
"value": (set the field to this value to select this option),
|
||||
"text": (display text of the option)
|
||||
},
|
||||
// Other choice options
|
||||
],
|
||||
}
|
||||
]
|
||||
```
|
||||
- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory):
|
||||
`python scripts/convert_pdf_to_images.py <file.pdf> <output_directory>`
|
||||
Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates).
|
||||
- Create a `field_values.json` file in this format with the values to be entered for each field:
|
||||
```
|
||||
[
|
||||
{
|
||||
"field_id": "last_name", // Must match the field_id from `extract_form_field_info.py`
|
||||
"description": "The user's last name",
|
||||
"page": 1, // Must match the "page" value in field_info.json
|
||||
"value": "Simpson"
|
||||
},
|
||||
{
|
||||
"field_id": "Checkbox12",
|
||||
"description": "Checkbox to be checked if the user is 18 or over",
|
||||
"page": 1,
|
||||
"value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options".
|
||||
},
|
||||
// more fields
|
||||
]
|
||||
```
|
||||
- Run the `fill_fillable_fields.py` script from this file's directory to create a filled-in PDF:
|
||||
`python scripts/fill_fillable_fields.py <input pdf> <field_values.json> <output pdf>`
|
||||
This script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again.
|
||||
|
||||
# Non-fillable fields
|
||||
If the PDF doesn't have fillable form fields, you'll need to visually determine where the data should be added and create text annotations. Follow the below steps *exactly*. You MUST perform all of these steps to ensure that the the form is accurately completed. Details for each step are below.
|
||||
- Convert the PDF to PNG images and determine field bounding boxes.
|
||||
- Create a JSON file with field information and validation images showing the bounding boxes.
|
||||
- Validate the the bounding boxes.
|
||||
- Use the bounding boxes to fill in the form.
|
||||
|
||||
## Step 1: Visual Analysis (REQUIRED)
|
||||
- Convert the PDF to PNG images. Run this script from this file's directory:
|
||||
`python scripts/convert_pdf_to_images.py <file.pdf> <output_directory>`
|
||||
The script will create a PNG image for each page in the PDF.
|
||||
- Carefully examine each PNG image and identify all form fields and areas where the user should enter data. For each form field where the user should enter text, determine bounding boxes for both the form field label, and the area where the user should enter text. The label and entry bounding boxes MUST NOT INTERSECT; the text entry box should only include the area where data should be entered. Usually this area will be immediately to the side, above, or below its label. Entry bounding boxes must be tall and wide enough to contain their text.
|
||||
|
||||
These are some examples of form structures that you might see:
|
||||
|
||||
*Label inside box*
|
||||
```
|
||||
┌────────────────────────┐
|
||||
│ Name: │
|
||||
└────────────────────────┘
|
||||
```
|
||||
The input area should be to the right of the "Name" label and extend to the edge of the box.
|
||||
|
||||
*Label before line*
|
||||
```
|
||||
Email: _______________________
|
||||
```
|
||||
The input area should be above the line and include its entire width.
|
||||
|
||||
*Label under line*
|
||||
```
|
||||
_________________________
|
||||
Name
|
||||
```
|
||||
The input area should be above the line and include the entire width of the line. This is common for signature and date fields.
|
||||
|
||||
*Label above line*
|
||||
```
|
||||
Please enter any special requests:
|
||||
________________________________________________
|
||||
```
|
||||
The input area should extend from the bottom of the label to the line, and should include the entire width of the line.
|
||||
|
||||
*Checkboxes*
|
||||
```
|
||||
Are you a US citizen? Yes □ No □
|
||||
```
|
||||
For checkboxes:
|
||||
- Look for small square boxes (□) - these are the actual checkboxes to target. They may be to the left or right of their labels.
|
||||
- Distinguish between label text ("Yes", "No") and the clickable checkbox squares.
|
||||
- The entry bounding box should cover ONLY the small square, not the text label.
|
||||
|
||||
### Step 2: Create fields.json and validation images (REQUIRED)
|
||||
- Create a file named `fields.json` with information for the form fields and bounding boxes in this format:
|
||||
```
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"page_number": 1,
|
||||
"image_width": (first page image width in pixels),
|
||||
"image_height": (first page image height in pixels),
|
||||
},
|
||||
{
|
||||
"page_number": 2,
|
||||
"image_width": (second page image width in pixels),
|
||||
"image_height": (second page image height in pixels),
|
||||
}
|
||||
// additional pages
|
||||
],
|
||||
"form_fields": [
|
||||
// Example for a text field.
|
||||
{
|
||||
"page_number": 1,
|
||||
"description": "The user's last name should be entered here",
|
||||
// Bounding boxes are [left, top, right, bottom]. The bounding boxes for the label and text entry should not overlap.
|
||||
"field_label": "Last name",
|
||||
"label_bounding_box": [30, 125, 95, 142],
|
||||
"entry_bounding_box": [100, 125, 280, 142],
|
||||
"entry_text": {
|
||||
"text": "Johnson", // This text will be added as an annotation at the entry_bounding_box location
|
||||
"font_size": 14, // optional, defaults to 14
|
||||
"font_color": "000000", // optional, RRGGBB format, defaults to 000000 (black)
|
||||
}
|
||||
},
|
||||
// Example for a checkbox. TARGET THE SQUARE for the entry bounding box, NOT THE TEXT
|
||||
{
|
||||
"page_number": 2,
|
||||
"description": "Checkbox that should be checked if the user is over 18",
|
||||
"entry_bounding_box": [140, 525, 155, 540], // Small box over checkbox square
|
||||
"field_label": "Yes",
|
||||
"label_bounding_box": [100, 525, 132, 540], // Box containing "Yes" text
|
||||
// Use "X" to check a checkbox.
|
||||
"entry_text": {
|
||||
"text": "X",
|
||||
}
|
||||
}
|
||||
// additional form field entries
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Create validation images by running this script from this file's directory for each page:
|
||||
`python scripts/create_validation_image.py <page_number> <path_to_fields.json> <input_image_path> <output_image_path>
|
||||
|
||||
The validation images will have red rectangles where text should be entered, and blue rectangles covering label text.
|
||||
|
||||
### Step 3: Validate Bounding Boxes (REQUIRED)
|
||||
#### Automated intersection check
|
||||
- Verify that none of bounding boxes intersect and that the entry bounding boxes are tall enough by checking the fields.json file with the `check_bounding_boxes.py` script (run from this file's directory):
|
||||
`python scripts/check_bounding_boxes.py <JSON file>`
|
||||
|
||||
If there are errors, reanalyze the relevant fields, adjust the bounding boxes, and iterate until there are no remaining errors. Remember: label (blue) bounding boxes should contain text labels, entry (red) boxes should not.
|
||||
|
||||
#### Manual image inspection
|
||||
**CRITICAL: Do not proceed without visually inspecting validation images**
|
||||
- Red rectangles must ONLY cover input areas
|
||||
- Red rectangles MUST NOT contain any text
|
||||
- Blue rectangles should contain label text
|
||||
- For checkboxes:
|
||||
- Red rectangle MUST be centered on the checkbox square
|
||||
- Blue rectangle should cover the text label for the checkbox
|
||||
|
||||
- If any rectangles look wrong, fix fields.json, regenerate the validation images, and verify again. Repeat this process until the bounding boxes are fully accurate.
|
||||
|
||||
|
||||
### Step 4: Add annotations to the PDF
|
||||
Run this script from this file's directory to create a filled-out PDF using the information in fields.json:
|
||||
`python scripts/fill_pdf_form_with_annotations.py <input_pdf_path> <path_to_fields.json> <output_pdf_path>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,70 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
# Script to check that the `fields.json` file that Claude creates when analyzing PDFs
|
||||
# does not have overlapping bounding boxes. See forms.md.
|
||||
|
||||
|
||||
@dataclass
|
||||
class RectAndField:
|
||||
rect: list[float]
|
||||
rect_type: str
|
||||
field: dict
|
||||
|
||||
|
||||
# Returns a list of messages that are printed to stdout for Claude to read.
|
||||
def get_bounding_box_messages(fields_json_stream) -> list[str]:
|
||||
messages = []
|
||||
fields = json.load(fields_json_stream)
|
||||
messages.append(f"Read {len(fields['form_fields'])} fields")
|
||||
|
||||
def rects_intersect(r1, r2):
|
||||
disjoint_horizontal = r1[0] >= r2[2] or r1[2] <= r2[0]
|
||||
disjoint_vertical = r1[1] >= r2[3] or r1[3] <= r2[1]
|
||||
return not (disjoint_horizontal or disjoint_vertical)
|
||||
|
||||
rects_and_fields = []
|
||||
for f in fields["form_fields"]:
|
||||
rects_and_fields.append(RectAndField(f["label_bounding_box"], "label", f))
|
||||
rects_and_fields.append(RectAndField(f["entry_bounding_box"], "entry", f))
|
||||
|
||||
has_error = False
|
||||
for i, ri in enumerate(rects_and_fields):
|
||||
# This is O(N^2); we can optimize if it becomes a problem.
|
||||
for j in range(i + 1, len(rects_and_fields)):
|
||||
rj = rects_and_fields[j]
|
||||
if ri.field["page_number"] == rj.field["page_number"] and rects_intersect(ri.rect, rj.rect):
|
||||
has_error = True
|
||||
if ri.field is rj.field:
|
||||
messages.append(f"FAILURE: intersection between label and entry bounding boxes for `{ri.field['description']}` ({ri.rect}, {rj.rect})")
|
||||
else:
|
||||
messages.append(f"FAILURE: intersection between {ri.rect_type} bounding box for `{ri.field['description']}` ({ri.rect}) and {rj.rect_type} bounding box for `{rj.field['description']}` ({rj.rect})")
|
||||
if len(messages) >= 20:
|
||||
messages.append("Aborting further checks; fix bounding boxes and try again")
|
||||
return messages
|
||||
if ri.rect_type == "entry":
|
||||
if "entry_text" in ri.field:
|
||||
font_size = ri.field["entry_text"].get("font_size", 14)
|
||||
entry_height = ri.rect[3] - ri.rect[1]
|
||||
if entry_height < font_size:
|
||||
has_error = True
|
||||
messages.append(f"FAILURE: entry bounding box height ({entry_height}) for `{ri.field['description']}` is too short for the text content (font size: {font_size}). Increase the box height or decrease the font size.")
|
||||
if len(messages) >= 20:
|
||||
messages.append("Aborting further checks; fix bounding boxes and try again")
|
||||
return messages
|
||||
|
||||
if not has_error:
|
||||
messages.append("SUCCESS: All bounding boxes are valid")
|
||||
return messages
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: check_bounding_boxes.py [fields.json]")
|
||||
sys.exit(1)
|
||||
# Input file should be in the `fields.json` format described in forms.md.
|
||||
with open(sys.argv[1]) as f:
|
||||
messages = get_bounding_box_messages(f)
|
||||
for msg in messages:
|
||||
print(msg)
|
||||
@@ -1,226 +0,0 @@
|
||||
import unittest
|
||||
import json
|
||||
import io
|
||||
from check_bounding_boxes import get_bounding_box_messages
|
||||
|
||||
|
||||
# Currently this is not run automatically in CI; it's just for documentation and manual checking.
|
||||
class TestGetBoundingBoxMessages(unittest.TestCase):
|
||||
|
||||
def create_json_stream(self, data):
|
||||
"""Helper to create a JSON stream from data"""
|
||||
return io.StringIO(json.dumps(data))
|
||||
|
||||
def test_no_intersections(self):
|
||||
"""Test case with no bounding box intersections"""
|
||||
data = {
|
||||
"form_fields": [
|
||||
{
|
||||
"description": "Name",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 50, 30],
|
||||
"entry_bounding_box": [60, 10, 150, 30]
|
||||
},
|
||||
{
|
||||
"description": "Email",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 40, 50, 60],
|
||||
"entry_bounding_box": [60, 40, 150, 60]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
self.assertTrue(any("SUCCESS" in msg for msg in messages))
|
||||
self.assertFalse(any("FAILURE" in msg for msg in messages))
|
||||
|
||||
def test_label_entry_intersection_same_field(self):
|
||||
"""Test intersection between label and entry of the same field"""
|
||||
data = {
|
||||
"form_fields": [
|
||||
{
|
||||
"description": "Name",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 60, 30],
|
||||
"entry_bounding_box": [50, 10, 150, 30] # Overlaps with label
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
self.assertTrue(any("FAILURE" in msg and "intersection" in msg for msg in messages))
|
||||
self.assertFalse(any("SUCCESS" in msg for msg in messages))
|
||||
|
||||
def test_intersection_between_different_fields(self):
|
||||
"""Test intersection between bounding boxes of different fields"""
|
||||
data = {
|
||||
"form_fields": [
|
||||
{
|
||||
"description": "Name",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 50, 30],
|
||||
"entry_bounding_box": [60, 10, 150, 30]
|
||||
},
|
||||
{
|
||||
"description": "Email",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [40, 20, 80, 40], # Overlaps with Name's boxes
|
||||
"entry_bounding_box": [160, 10, 250, 30]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
self.assertTrue(any("FAILURE" in msg and "intersection" in msg for msg in messages))
|
||||
self.assertFalse(any("SUCCESS" in msg for msg in messages))
|
||||
|
||||
def test_different_pages_no_intersection(self):
|
||||
"""Test that boxes on different pages don't count as intersecting"""
|
||||
data = {
|
||||
"form_fields": [
|
||||
{
|
||||
"description": "Name",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 50, 30],
|
||||
"entry_bounding_box": [60, 10, 150, 30]
|
||||
},
|
||||
{
|
||||
"description": "Email",
|
||||
"page_number": 2,
|
||||
"label_bounding_box": [10, 10, 50, 30], # Same coordinates but different page
|
||||
"entry_bounding_box": [60, 10, 150, 30]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
self.assertTrue(any("SUCCESS" in msg for msg in messages))
|
||||
self.assertFalse(any("FAILURE" in msg for msg in messages))
|
||||
|
||||
def test_entry_height_too_small(self):
|
||||
"""Test that entry box height is checked against font size"""
|
||||
data = {
|
||||
"form_fields": [
|
||||
{
|
||||
"description": "Name",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 50, 30],
|
||||
"entry_bounding_box": [60, 10, 150, 20], # Height is 10
|
||||
"entry_text": {
|
||||
"font_size": 14 # Font size larger than height
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
self.assertTrue(any("FAILURE" in msg and "height" in msg for msg in messages))
|
||||
self.assertFalse(any("SUCCESS" in msg for msg in messages))
|
||||
|
||||
def test_entry_height_adequate(self):
|
||||
"""Test that adequate entry box height passes"""
|
||||
data = {
|
||||
"form_fields": [
|
||||
{
|
||||
"description": "Name",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 50, 30],
|
||||
"entry_bounding_box": [60, 10, 150, 30], # Height is 20
|
||||
"entry_text": {
|
||||
"font_size": 14 # Font size smaller than height
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
self.assertTrue(any("SUCCESS" in msg for msg in messages))
|
||||
self.assertFalse(any("FAILURE" in msg for msg in messages))
|
||||
|
||||
def test_default_font_size(self):
|
||||
"""Test that default font size is used when not specified"""
|
||||
data = {
|
||||
"form_fields": [
|
||||
{
|
||||
"description": "Name",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 50, 30],
|
||||
"entry_bounding_box": [60, 10, 150, 20], # Height is 10
|
||||
"entry_text": {} # No font_size specified, should use default 14
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
self.assertTrue(any("FAILURE" in msg and "height" in msg for msg in messages))
|
||||
self.assertFalse(any("SUCCESS" in msg for msg in messages))
|
||||
|
||||
def test_no_entry_text(self):
|
||||
"""Test that missing entry_text doesn't cause height check"""
|
||||
data = {
|
||||
"form_fields": [
|
||||
{
|
||||
"description": "Name",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 50, 30],
|
||||
"entry_bounding_box": [60, 10, 150, 20] # Small height but no entry_text
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
self.assertTrue(any("SUCCESS" in msg for msg in messages))
|
||||
self.assertFalse(any("FAILURE" in msg for msg in messages))
|
||||
|
||||
def test_multiple_errors_limit(self):
|
||||
"""Test that error messages are limited to prevent excessive output"""
|
||||
fields = []
|
||||
# Create many overlapping fields
|
||||
for i in range(25):
|
||||
fields.append({
|
||||
"description": f"Field{i}",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 50, 30], # All overlap
|
||||
"entry_bounding_box": [20, 15, 60, 35] # All overlap
|
||||
})
|
||||
|
||||
data = {"form_fields": fields}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
# Should abort after ~20 messages
|
||||
self.assertTrue(any("Aborting" in msg for msg in messages))
|
||||
# Should have some FAILURE messages but not hundreds
|
||||
failure_count = sum(1 for msg in messages if "FAILURE" in msg)
|
||||
self.assertGreater(failure_count, 0)
|
||||
self.assertLess(len(messages), 30) # Should be limited
|
||||
|
||||
def test_edge_touching_boxes(self):
|
||||
"""Test that boxes touching at edges don't count as intersecting"""
|
||||
data = {
|
||||
"form_fields": [
|
||||
{
|
||||
"description": "Name",
|
||||
"page_number": 1,
|
||||
"label_bounding_box": [10, 10, 50, 30],
|
||||
"entry_bounding_box": [50, 10, 150, 30] # Touches at x=50
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stream = self.create_json_stream(data)
|
||||
messages = get_bounding_box_messages(stream)
|
||||
self.assertTrue(any("SUCCESS" in msg for msg in messages))
|
||||
self.assertFalse(any("FAILURE" in msg for msg in messages))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,12 +0,0 @@
|
||||
import sys
|
||||
from pypdf import PdfReader
|
||||
|
||||
|
||||
# Script for Claude to run to determine whether a PDF has fillable form fields. See forms.md.
|
||||
|
||||
|
||||
reader = PdfReader(sys.argv[1])
|
||||
if (reader.get_fields()):
|
||||
print("This PDF has fillable form fields")
|
||||
else:
|
||||
print("This PDF does not have fillable form fields; you will need to visually determine where to enter data")
|
||||
@@ -1,35 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pdf2image import convert_from_path
|
||||
|
||||
|
||||
# Converts each page of a PDF to a PNG image.
|
||||
|
||||
|
||||
def convert(pdf_path, output_dir, max_dim=1000):
|
||||
images = convert_from_path(pdf_path, dpi=200)
|
||||
|
||||
for i, image in enumerate(images):
|
||||
# Scale image if needed to keep width/height under `max_dim`
|
||||
width, height = image.size
|
||||
if width > max_dim or height > max_dim:
|
||||
scale_factor = min(max_dim / width, max_dim / height)
|
||||
new_width = int(width * scale_factor)
|
||||
new_height = int(height * scale_factor)
|
||||
image = image.resize((new_width, new_height))
|
||||
|
||||
image_path = os.path.join(output_dir, f"page_{i+1}.png")
|
||||
image.save(image_path)
|
||||
print(f"Saved page {i+1} as {image_path} (size: {image.size})")
|
||||
|
||||
print(f"Converted {len(images)} pages to PNG images")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: convert_pdf_to_images.py [input pdf] [output directory]")
|
||||
sys.exit(1)
|
||||
pdf_path = sys.argv[1]
|
||||
output_directory = sys.argv[2]
|
||||
convert(pdf_path, output_directory)
|
||||
@@ -1,41 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
# Creates "validation" images with rectangles for the bounding box information that
|
||||
# Claude creates when determining where to add text annotations in PDFs. See forms.md.
|
||||
|
||||
|
||||
def create_validation_image(page_number, fields_json_path, input_path, output_path):
|
||||
# Input file should be in the `fields.json` format described in forms.md.
|
||||
with open(fields_json_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
img = Image.open(input_path)
|
||||
draw = ImageDraw.Draw(img)
|
||||
num_boxes = 0
|
||||
|
||||
for field in data["form_fields"]:
|
||||
if field["page_number"] == page_number:
|
||||
entry_box = field['entry_bounding_box']
|
||||
label_box = field['label_bounding_box']
|
||||
# Draw red rectangle over entry bounding box and blue rectangle over the label.
|
||||
draw.rectangle(entry_box, outline='red', width=2)
|
||||
draw.rectangle(label_box, outline='blue', width=2)
|
||||
num_boxes += 2
|
||||
|
||||
img.save(output_path)
|
||||
print(f"Created validation image at {output_path} with {num_boxes} bounding boxes")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 5:
|
||||
print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]")
|
||||
sys.exit(1)
|
||||
page_number = int(sys.argv[1])
|
||||
fields_json_path = sys.argv[2]
|
||||
input_image_path = sys.argv[3]
|
||||
output_image_path = sys.argv[4]
|
||||
create_validation_image(page_number, fields_json_path, input_image_path, output_image_path)
|
||||
@@ -1,152 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from pypdf import PdfReader
|
||||
|
||||
|
||||
# Extracts data for the fillable form fields in a PDF and outputs JSON that
|
||||
# Claude uses to fill the fields. See forms.md.
|
||||
|
||||
|
||||
# This matches the format used by PdfReader `get_fields` and `update_page_form_field_values` methods.
|
||||
def get_full_annotation_field_id(annotation):
|
||||
components = []
|
||||
while annotation:
|
||||
field_name = annotation.get('/T')
|
||||
if field_name:
|
||||
components.append(field_name)
|
||||
annotation = annotation.get('/Parent')
|
||||
return ".".join(reversed(components)) if components else None
|
||||
|
||||
|
||||
def make_field_dict(field, field_id):
|
||||
field_dict = {"field_id": field_id}
|
||||
ft = field.get('/FT')
|
||||
if ft == "/Tx":
|
||||
field_dict["type"] = "text"
|
||||
elif ft == "/Btn":
|
||||
field_dict["type"] = "checkbox" # radio groups handled separately
|
||||
states = field.get("/_States_", [])
|
||||
if len(states) == 2:
|
||||
# "/Off" seems to always be the unchecked value, as suggested by
|
||||
# https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf#page=448
|
||||
# It can be either first or second in the "/_States_" list.
|
||||
if "/Off" in states:
|
||||
field_dict["checked_value"] = states[0] if states[0] != "/Off" else states[1]
|
||||
field_dict["unchecked_value"] = "/Off"
|
||||
else:
|
||||
print(f"Unexpected state values for checkbox `${field_id}`. Its checked and unchecked values may not be correct; if you're trying to check it, visually verify the results.")
|
||||
field_dict["checked_value"] = states[0]
|
||||
field_dict["unchecked_value"] = states[1]
|
||||
elif ft == "/Ch":
|
||||
field_dict["type"] = "choice"
|
||||
states = field.get("/_States_", [])
|
||||
field_dict["choice_options"] = [{
|
||||
"value": state[0],
|
||||
"text": state[1],
|
||||
} for state in states]
|
||||
else:
|
||||
field_dict["type"] = f"unknown ({ft})"
|
||||
return field_dict
|
||||
|
||||
|
||||
# Returns a list of fillable PDF fields:
|
||||
# [
|
||||
# {
|
||||
# "field_id": "name",
|
||||
# "page": 1,
|
||||
# "type": ("text", "checkbox", "radio_group", or "choice")
|
||||
# // Per-type additional fields described in forms.md
|
||||
# },
|
||||
# ]
|
||||
def get_field_info(reader: PdfReader):
|
||||
fields = reader.get_fields()
|
||||
|
||||
field_info_by_id = {}
|
||||
possible_radio_names = set()
|
||||
|
||||
for field_id, field in fields.items():
|
||||
# Skip if this is a container field with children, except that it might be
|
||||
# a parent group for radio button options.
|
||||
if field.get("/Kids"):
|
||||
if field.get("/FT") == "/Btn":
|
||||
possible_radio_names.add(field_id)
|
||||
continue
|
||||
field_info_by_id[field_id] = make_field_dict(field, field_id)
|
||||
|
||||
# Bounding rects are stored in annotations in page objects.
|
||||
|
||||
# Radio button options have a separate annotation for each choice;
|
||||
# all choices have the same field name.
|
||||
# See https://westhealth.github.io/exploring-fillable-forms-with-pdfrw.html
|
||||
radio_fields_by_id = {}
|
||||
|
||||
for page_index, page in enumerate(reader.pages):
|
||||
annotations = page.get('/Annots', [])
|
||||
for ann in annotations:
|
||||
field_id = get_full_annotation_field_id(ann)
|
||||
if field_id in field_info_by_id:
|
||||
field_info_by_id[field_id]["page"] = page_index + 1
|
||||
field_info_by_id[field_id]["rect"] = ann.get('/Rect')
|
||||
elif field_id in possible_radio_names:
|
||||
try:
|
||||
# ann['/AP']['/N'] should have two items. One of them is '/Off',
|
||||
# the other is the active value.
|
||||
on_values = [v for v in ann["/AP"]["/N"] if v != "/Off"]
|
||||
except KeyError:
|
||||
continue
|
||||
if len(on_values) == 1:
|
||||
rect = ann.get("/Rect")
|
||||
if field_id not in radio_fields_by_id:
|
||||
radio_fields_by_id[field_id] = {
|
||||
"field_id": field_id,
|
||||
"type": "radio_group",
|
||||
"page": page_index + 1,
|
||||
"radio_options": [],
|
||||
}
|
||||
# Note: at least on macOS 15.7, Preview.app doesn't show selected
|
||||
# radio buttons correctly. (It does if you remove the leading slash
|
||||
# from the value, but that causes them not to appear correctly in
|
||||
# Chrome/Firefox/Acrobat/etc).
|
||||
radio_fields_by_id[field_id]["radio_options"].append({
|
||||
"value": on_values[0],
|
||||
"rect": rect,
|
||||
})
|
||||
|
||||
# Some PDFs have form field definitions without corresponding annotations,
|
||||
# so we can't tell where they are. Ignore these fields for now.
|
||||
fields_with_location = []
|
||||
for field_info in field_info_by_id.values():
|
||||
if "page" in field_info:
|
||||
fields_with_location.append(field_info)
|
||||
else:
|
||||
print(f"Unable to determine location for field id: {field_info.get('field_id')}, ignoring")
|
||||
|
||||
# Sort by page number, then Y position (flipped in PDF coordinate system), then X.
|
||||
def sort_key(f):
|
||||
if "radio_options" in f:
|
||||
rect = f["radio_options"][0]["rect"] or [0, 0, 0, 0]
|
||||
else:
|
||||
rect = f.get("rect") or [0, 0, 0, 0]
|
||||
adjusted_position = [-rect[1], rect[0]]
|
||||
return [f.get("page"), adjusted_position]
|
||||
|
||||
sorted_fields = fields_with_location + list(radio_fields_by_id.values())
|
||||
sorted_fields.sort(key=sort_key)
|
||||
|
||||
return sorted_fields
|
||||
|
||||
|
||||
def write_field_info(pdf_path: str, json_output_path: str):
|
||||
reader = PdfReader(pdf_path)
|
||||
field_info = get_field_info(reader)
|
||||
with open(json_output_path, "w") as f:
|
||||
json.dump(field_info, f, indent=2)
|
||||
print(f"Wrote {len(field_info)} fields to {json_output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: extract_form_field_info.py [input pdf] [output json]")
|
||||
sys.exit(1)
|
||||
write_field_info(sys.argv[1], sys.argv[2])
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user