agents
"""Agent definitions for Cursor Prompt Preprocessor.
This module contains all the LLM agents used in the Cursor Prompt Preprocessor,
defining their names, prompts, and tools.
"""
# 模块的文档字符串:定义了 Cursor Prompt Preprocessor 中使用的所有 Agent。
# 描述了模块的作用:包含 LLM Agent 的定义,包括它们的名称、提示词和工具。
# 导入 Google ADK 中的 Agent 类
from google.adk.agents import LlmAgent, SequentialAgent, ParallelAgent
# 导入 ADK 中的循环 Agent 类
from google.adk.agents.loop_agent import LoopAgent
# 导入 ADK 中的函数工具类,用于将 Python 函数包装成 Agent 可用的工具
from google.adk.tools import FunctionTool
# 从我们自己的模块导入配置和工具
# 从配置文件导入常量
from cursor_prompt_preprocessor.config import (
GEMINI_MODEL, # 使用的 Gemini 模型名称
STATE_USER_PROMPT, # 共享状态键:用户原始提示词
STATE_PROJECT_STRUCTURE, # 共享状态键:项目结构总结
STATE_DEPENDENCIES, # 共享状态键:项目依赖分析结果
STATE_FILTERED_STRUCTURE, # 共享状态键:应用 .gitignore 过滤后的项目结构
STATE_RELEVANT_CODE, # 共享状态键:搜索到的相关代码文件
STATE_RELEVANT_TESTS, # 共享状态键:搜索到的相关测试文件
STATE_RELEVANCE_SCORES, # 共享状态键:相关性评分或排序结果
STATE_QUESTIONS, # 共享状态键:Agent 生成的澄清问题
STATE_ANSWERS, # 共享状态键:用户对澄清问题的回答列表
STATE_FINAL_CONTEXT, # 共享状态键:最终合成的上下文信息
STATE_TARGET_DIRECTORY, # 共享状态键:用户指定的目标目录
NO_QUESTIONS # 常量:表示没有澄清问题需要询问
)
# 从日志设置模块导入 logger 实例
from cursor_prompt_preprocessor.logging_setup import logger
# 从速率限制模块导入速率限制处理函数
from cursor_prompt_preprocessor.rate_limiting import pre_model_rate_limit, handle_rate_limit
# 从工具模块导入具体的工具函数
from cursor_prompt_preprocessor.tools import (
scan_project_structure, # 工具函数:扫描项目目录结构
get_dependencies, # 工具函数:获取项目依赖信息
apply_gitignore_filter, # 工具函数:根据 .gitignore 过滤文件
search_code_with_prompt, # 工具函数:根据提示词搜索代码
search_tests_with_prompt, # 工具函数:根据提示词搜索测试
determine_relevance_from_prompt, # 工具函数:根据提示词确定相关性(可能用于指导 Agent 如何判断)
set_state, # 工具函数:设置共享状态中的某个键值
set_target_directory, # 工具函数:设置目标目录
read_file_content, # 工具函数:读取文件内容
list_directory_contents, # 工具函数:列出目录内容
ClarifierGenerator # 工具类/函数:用于生成澄清问题交互工具
)
# 定义一个创建带有速率限制功能的 Agent 的工厂函数
def create_rate_limited_agent(name, model, instruction, tools=None, output_key=None, sub_agents=None):
"""Create an LlmAgent with rate limiting applied.
Factory function to ensure all agents have consistent rate limiting.
Args:
name: Agent name
model: Model name
instruction: Agent instructions
tools: List of tools
output_key: Output state key
sub_agents: List of sub-agents
Returns:
LlmAgent with rate limiting applied
"""
# 返回一个 LlmAgent 实例
return LlmAgent(
name=name, # Agent 名称
model=model, # 使用的 LLM 模型
instruction=instruction, # 给 LLM 的指令,定义 Agent 的任务和行为
tools=tools or [], # Agent 可用的工具列表,如果没有提供则为空列表
output_key=output_key, # Agent 输出结果存储在共享状态中的键
sub_agents=sub_agents or [], # Agent 的子 Agent 列表(如果Agent是Sequential或Parallel等)
before_model_callback=pre_model_rate_limit, # 在调用 LLM 之前执行的回调函数(用于速率限制)
after_model_callback=handle_rate_limit # 在调用 LLM 之后执行的回调函数(用于速率限制)
)
# --- Tool Wrappers ---
# 工具封装 - 将具体的 Python 函数包装成 FunctionTool 实例,供 Agent 调用
# 将 scan_project_structure 函数包装成 FunctionTool
scan_project_structure_tool = FunctionTool(func=scan_project_structure)
# 将 get_dependencies 函数包装成 FunctionTool
get_dependencies_tool = FunctionTool(func=get_dependencies)
# 将 apply_gitignore_filter 函数包装成 FunctionTool
apply_gitignore_filter_tool = FunctionTool(func=apply_gitignore_filter)
# 将 read_file_content 函数包装成 FunctionTool
read_file_content_tool = FunctionTool(func=read_file_content)
# 将 list_directory_contents 函数包装成 FunctionTool
list_directory_contents_tool = FunctionTool(func=list_directory_contents)
# 将 search_code_with_prompt 函数包装成 FunctionTool
search_code_with_prompt_tool = FunctionTool(func=search_code_with_prompt)
# 将 search_tests_with_prompt 函数包装成 FunctionTool
search_tests_with_prompt_tool = FunctionTool(func=search_tests_with_prompt)
# 将 determine_relevance_from_prompt 函数包装成 FunctionTool
determine_relevance_from_prompt_tool = FunctionTool(func=determine_relevance_from_prompt)
# 将 set_state 函数包装成 FunctionTool
set_state_tool = FunctionTool(func=set_state)
# 将 set_target_directory 函数包装成 FunctionTool
set_target_directory_tool = FunctionTool(func=set_target_directory)
# 将 ClarifierGenerator 的实例包装成 FunctionTool (可能用于生成交互工具实例)
clarify_questions_tool = FunctionTool(func=ClarifierGenerator())
# --- LLM Agents ---
# LLM Agent 定义 - 定义使用 LLM 提供能力的独立 Agent
# 项目结构分析 Agent
project_structure_agent = create_rate_limited_agent(
name="ProjectStructureAgent", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction="""
You are a Project Structure Analyzer.
Your task is to scan the project directory structure provided in the session state
and summarize the key components and organization of the project.
Focus on identifying:
1. Main source code directories
2. Test directories
3. Configuration files
4. Documentation
5. Resource files
6. Any other important files, given the context of the projext
Format your response as a structured summary that would help a developer understand
the project's organization. Focus on important aspects of the structure that would
help with understanding the codebase.
""", # Agent 的指令:扫描项目结构,总结关键组件和组织方式,关注重要目录和文件。
tools=[scan_project_structure_tool], # 可用的工具:扫描项目结构
output_key=STATE_PROJECT_STRUCTURE # 输出存储在 STATE_PROJECT_STRUCTURE 键中
)
# 依赖分析 Agent
dependency_analysis_agent = create_rate_limited_agent(
name="DependencyAnalysisAgent", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction="""
You are a Dependency Analyzer.
Your task is to analyze the project dependencies from the session state and provide insights
about the technologies and frameworks used in the project.
Focus on:
1. Identifying the main frameworks/libraries
2. Noting any version constraints
3. Recognizing patterns in the dependencies that indicate the project type
4. Flagging any potential issues (outdated dependencies, known incompatibilities, etc.)
5. If the project is a monorepo, identify the main packages and their dependencies
Format your response as a concise analysis that would help a developer understand
the technological stack of the project.
""", # Agent 的指令:分析项目依赖,识别技术栈、框架、版本等,提供简洁分析。
tools=[list_directory_contents_tool, read_file_content_tool], # 可用的工具:列出目录内容,读取文件内容(可能用于读取依赖配置文件)
output_key=STATE_DEPENDENCIES # 输出存储在 STATE_DEPENDENCIES 键中
)
# Gitignore 过滤 Agent
gitignore_filter_agent = create_rate_limited_agent(
name="GitignoreFilterAgent", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction="""
You are a Gitignore Filter Agent.
Your task is to filter the project structure based on the project's gitignore rules.
This helps focus on relevant code files and exclude build artifacts, caches, etc.
Call the apply_gitignore_filter function to get the filtered project structure.
Return the filtered project structure showing only the files and directories that
would typically be relevant for understanding the codebase.
""", # Agent 的指令:根据 .gitignore 规则过滤项目结构,只保留与代码理解相关的部分。明确指示调用 apply_gitignore_filter 工具。
tools=[apply_gitignore_filter_tool, read_file_content_tool], # 可用的工具:应用 .gitignore 过滤,读取文件内容(可能用于读取 .gitignore 文件)
output_key=STATE_FILTERED_STRUCTURE # 输出存储在 STATE_FILTERED_STRUCTURE 键中
)
# 代码搜索 Agent
code_search_agent = create_rate_limited_agent(
name="CodeSearchAgent", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction=f"""
You are a Code Search Specialist.
Your task is to extract keywords from the user's prompt in the state key '{STATE_USER_PROMPT}'
and use them to find relevant code files in the project, given the code structure
in the state key {STATE_FILTERED_STRUCTURE}.
1. Please extract keywords from the user prompt in the session state and use them to search the codebase.
You can also pass the raw prompt to help with keyword extraction.
2. Extract 3-5 key technical terms or concepts from the prompt stored in '{STATE_USER_PROMPT}'
3. Use your analysis to find relevant code files using tool search_code_with_prompt()
Format your response as a clear summary of the most relevant code locations.
IMPORTANT: If needed, utilize the read_file_content() tool and list_directory_contents() tool to get more context about the codebase.
""", # Agent 的指令:从用户提示词中提取关键字,并在过滤后的项目结构中搜索相关代码文件。明确指示提取关键字、使用 search_code_with_prompt 工具,并可在需要时使用文件读取工具获取上下文。
tools=[
search_code_with_prompt_tool, # 可用的工具:根据提示词搜索代码
read_file_content_tool, # 可用的工具:读取文件内容
list_directory_contents_tool # 可用的工具:列出目录内容
],
output_key=STATE_RELEVANT_CODE # 输出存储在 STATE_RELEVANT_CODE 键中
)
# 测试搜索 Agent
test_search_agent = create_rate_limited_agent(
name="TestSearchAgent", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction=f"""
You are a Test Code Search Specialist.
Your task is to extract keywords from the user's prompt in the state key '{STATE_USER_PROMPT}'
and use them to find relevant test files in the project, given the code structure
in the state key {STATE_FILTERED_STRUCTURE}.
1. Please extract keywords from the user prompt in the session state and use them to search the test files.
You can also pass the raw prompt to help with keyword extraction.
2. Extract 3-5 key technical terms or concepts from the prompt stored in '{STATE_USER_PROMPT}'
3. Use your analysis to find relevant test files using tool search_tests_with_prompt()
Format your response as a clear summary of the most relevant test file locations.
IMPORTANT: If needed, utilize the read_file_content() tool and list_directory_contents() tool to get more context about the codebase.
""", # Agent 的指令:从用户提示词中提取关键字,并在过滤后的项目结构中搜索相关测试文件。明确指示提取关键字、使用 search_tests_with_prompt 工具,并可在需要时使用文件读取工具获取上下文。
tools=[
search_tests_with_prompt_tool, # 可用的工具:根据提示词搜索测试
read_file_content_tool, # 可用的工具:读取文件内容
list_directory_contents_tool # 可用的工具:列出目录内容
],
output_key=STATE_RELEVANT_TESTS # 输出存储在 STATE_RELEVANT_TESTS 键中
)
# 相关性判定 Agent
relevance_determination_agent = create_rate_limited_agent(
name="RelevanceDeterminationAgent", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction=f"""
You are a Relevance Analyst.
Your task is to analyze the code and test files found from the session state
('{STATE_RELEVANT_CODE}' and '{STATE_RELEVANT_TESTS}') and determine their
relevance to the user's prompt ('{STATE_USER_PROMPT}').
First, call determine_relevance_from_prompt() to get instructions.
Then:
1. Analyze the found code and test files in relation to the user's prompt
2. Assign relevance scores or rankings
3. Explain the rationale for the most relevant files
Format your response as a ranked list with explanations for why each top file is relevant.
""", # Agent 的指令:分析找到的代码和测试文件与用户提示词的相关性,进行评分/排序,并解释原因。明确指示先调用 determine_relevance_from_prompt 工具(用于获取如何判断相关性的指导),然后进行分析。
tools=[determine_relevance_from_prompt_tool, read_file_content_tool, list_directory_contents_tool], # 可用的工具:确定相关性指导,读取文件内容,列出目录内容(用于获取文件详情辅助判断)
output_key=STATE_RELEVANCE_SCORES # 输出存储在 STATE_RELEVANCE_SCORES 键中
)
# 问题询问 Agent
question_asking_agent = create_rate_limited_agent(
name="QuestionAskingAgent", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction=f"""
You are a Clarifying Question Generator.
Your task is to analyze the user's prompt from the state key '{STATE_USER_PROMPT}'
along with the project information gathered so far including your previous questions state key {STATE_QUESTIONS}
the state key '{STATE_ANSWERS}', and generate clarifying questions
when the prompt + clarifications are ambiguous or lacks necessary details.
The questions should help pinpoint exactly what the user needs in terms of code implementation.
If you think that the prompt doesn't make sense in the context of the project, explain why in your question (rules for them above)
The project might contain code that already satisfies, or partially satisfies the user's prompt.
If the user prompt is ambiguous or its assumptions contradict some of the project's information, explain why in your question and request clarification.
Do the following, in order:
1. Identify unclear aspects or missing information in the prompt.
Use the structure of the project to help you understand the user's prompt.
Use read_file_content() tool to clarify your doubts about the existing code before asking the user.
2. Formulate 1-3 specific, targeted questions to clarify these aspects
3. If you have questions to ask, respond with the questions you have.
4. If the prompt is completely clear and has sufficient information, respond EXACTLY with the string "{NO_QUESTIONS}"
""", # Agent 的指令:生成澄清问题。分析原始提示词、已有的项目信息、之前的问答,识别不明确或缺失的信息。问题应帮助明确代码实现需求。如果提示词与项目上下文冲突,解释原因。明确指示检查现有代码(使用 read_file_content),提出 1-3 个问题,或在明确时输出 NO_QUESTIONS 标记。
tools=[read_file_content_tool], # 可用的工具:读取文件内容(用于检查现有代码辅助判断)
output_key=STATE_QUESTIONS # 输出存储在 STATE_QUESTIONS 键中
)
# 用户答案收集 Agent
user_answer_collection_agent = create_rate_limited_agent(
name="UserAnswerCollectionAgent", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction=f"""
You are a User Answer Collector.
Your task is based on the content of the state key '{STATE_QUESTIONS}':
1. Check if the value in '{STATE_QUESTIONS}' is EXACTLY the string "{NO_QUESTIONS}".
2. Report clearly: State whether questions were found or not.
3. If questions exist (i.e., the state is NOT "{NO_QUESTIONS}"):
a. Announce that you will now ask for clarification via the console tool, showing the question stored in the state.
b. Use the `clarify_questions_tool` to get console input.
c. Retrieve the current list of answers from the state key '{STATE_ANSWERS}'
d. MUST Append the new 'reply' received from the tool to this list.
e. MUST Call `set_state` to store the updated list back into the '{STATE_ANSWERS}' state key.
4. If no questions exist (i.e., the state IS "{NO_QUESTIONS}"):
a. Announce that no clarification is needed and the loop should terminate.
b. Respond EXACTLY with the string "NO_CLARIFICATION_NEEDED_EXIT_LOOP"
Ensure you handle the state correctly, especially creating the list for '{STATE_ANSWERS}' if it's the first answer.
""", # Agent 的指令:根据 STATE_QUESTIONS 中的内容决定是收集用户答案还是终止循环。如果 STATE_QUESTIONS 不是 NO_QUESTIONS,则调用 clarify_questions_tool 获取用户输入,将答案添加到 STATE_ANSWERS 列表,并使用 set_state 更新状态。如果 STATE_QUESTIONS 是 NO_QUESTIONS,则输出特定字符串 "NO_CLARIFICATION_NEEDED_EXIT_LOOP" 通知循环终止。
tools=[
clarify_questions_tool, # 可用的工具:获取用户输入(可能通过控制台)
set_state_tool # 可用的工具:设置共享状态
],
output_key=STATE_ANSWERS # 输出存储在 STATE_ANSWERS 键中(主要是更新这个列表,但 Agent 的输出本身也会写入)
)
# 上下文形成 Agent
context_formation_agent = create_rate_limited_agent(
name="ContextFormationAgent", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction=f"""
You are a Context Formation Specialist.
Your task is to synthesize all the information gathered in the previous steps into
a comprehensive context object that will be used for code generation.
Compile a structured context that includes:
1. The initial user's prompt (from '{STATE_USER_PROMPT}')
2. Relevant project structure information (from '{STATE_PROJECT_STRUCTURE}')
3. Key dependencies (from '{STATE_DEPENDENCIES}')
4. The most relevant code files and snippets (from '{STATE_RELEVANT_CODE}')
5. The most relevant test files (from '{STATE_RELEVANT_TESTS}')
6. Any clarifying questions and their answers (from '{STATE_QUESTIONS}' and '{STATE_ANSWERS}')
7. REALLY IMPORTANT: your summarization of the user's prompt, given the clarifying questions and their answers.
Format your response as a well-structured context object with clear sections that a code
generation LLM would find helpful for understanding what needs to be implemented.
IMPORTANT: The user's prompt is the initial prompt, and the summarization is the final prompt, given the clarifying questions and their answers.
""", # Agent 的指令:合成所有已收集信息(原始提示词、项目信息、问答历史等)到一个结构化的上下文对象中,供后续代码生成使用。强调包含基于问答历史总结的最终提示词。
output_key=STATE_FINAL_CONTEXT # 输出存储在 STATE_FINAL_CONTEXT 键中
)
# --- Agent Pipeline Construction ---
# Agent 管道构建 - 组合 Agent 形成处理流程
# 用于搜索操作的并行 Agent
parallel_search_agent = ParallelAgent(
name="ParallelSearchAgent", # 管道名称
sub_agents=[code_search_agent, test_search_agent] # 包含的子 Agent,并行执行代码搜索和测试搜索
)
# 用于项目结构和依赖分析的顺序 Agent
structure_and_dependencies_agent = SequentialAgent(
name="StructureAndDependencies", # 管道名称
sub_agents=[
project_structure_agent, # 顺序执行:先分析项目结构
dependency_analysis_agent, # 然后分析依赖
gitignore_filter_agent # 最后进行 .gitignore 过滤
]
)
# 用于澄清过程的循环 Agent
clarification_and_decision_loop = LoopAgent(
name="ClarificationAndDecisionLoop", # 循环名称
sub_agents=[
parallel_search_agent, # 循环内顺序执行:并行搜索代码/测试
relevance_determination_agent, # 评估相关性
question_asking_agent, # 提出问题
user_answer_collection_agent # 收集答案并判定是否终止循环
],
max_iterations=3 # 最大循环次数,防止无限循环
)
# 用于上下文形成的顺序管道
context_former = SequentialAgent(
name="ContextFormer", # 管道名称
sub_agents=[
structure_and_dependencies_agent, # 顺序执行:先完成项目基础信息收集
clarification_and_decision_loop, # 然后进入澄清循环
context_formation_agent # 循环结束后,合成最终上下文
]
)
# 根 Agent (入口点)
root_agent = create_rate_limited_agent(
name="PromptProcessor", # Agent 名称
model=GEMINI_MODEL, # 使用的模型
instruction=f"""
You are the main coordinator for processing coding prompts.
Your first task is to:
1. Welcome the user
2. Store their coding prompt in the session state with key '{STATE_USER_PROMPT}' using the set_state tool
3. If a target directory was provided, acknowledge it
4. Transfer control to the ContextFormer agent
Be sure to always store the user's coding prompt in the state key '{STATE_USER_PROMPT}' before proceeding.
After the ContextFormer has completed, you should:
0. FLASH PRIORITY 0: write "AAAA DEBUG 001 001 001 PROMPT PROCESSOR HIT AFTER CONTEXT FORMER". # 调试用的特殊标记
1. Retrieve the final context from the session state
2. Present a concise summary of what was found and how it will help with the code generation
Keep your responses friendly, professional, and focused on helping the user succeed with their coding task.
""", # Agent 的指令:作为主协调器。首先欢迎用户,使用 set_state 将提示词存入 STATE_USER_PROMPT,处理目标目录,然后将控制权交给 context_former 子 Agent。在 context_former 完成后,检索最终上下文并向用户展示摘要。
tools=[set_state_tool, set_target_directory_tool], # 可用的工具:设置状态,设置目标目录(用于处理初始输入)
sub_agents=[context_former] # 包含的子 Agent:context_former(负责主要的处理流程)
)
# 代码结束,root_agent 是整个 Agent 系统的入口。
tools
# -*- coding: utf-8 -*-
# 导入所需的标准库模块
import datetime # 用于处理日期和时间
from zoneinfo import ZoneInfo # 用于处理时区信息
import glob # 用于查找符合特定模式的文件名
import os # 用于与操作系统进行交互,如文件路径操作
import gitignore_parser # 用于解析 .gitignore 文件
import json # 用于处理 JSON 数据
from typing import Optional, Dict, Any, List, Union # 用于类型提示
# 导入 Cursor Prompt Preprocessor 项目内部的配置和日志模块
# STATE_TARGET_DIRECTORY: 共享状态中存储目标目录的键
# STATE_QUESTIONS: 共享状态中存储待澄清问题的键
from cursor_prompt_preprocessor.config import STATE_TARGET_DIRECTORY, STATE_QUESTIONS
# logger: 用于记录日志信息
from cursor_prompt_preprocessor.logging_setup import logger
# session_manager: 用于管理 Agent 之间的共享状态
from cursor_prompt_preprocessor.session import session_manager
# --- 用户交互工具 ---
class ClarifierGenerator:
'''同步函数,用于获取控制台输入进行澄清。'''
__name__ = "clarify_questions_tool" # 为 Agent 指令定义的工具名称
def __call__(self) -> dict:
"""通过控制台输入从用户那里获取澄清。
从会话状态中检索问题,并提示用户输入。
Returns:
dict: 用户的回复,包含在字典中。
"""
# 从共享状态中获取待澄清的问题
# 如果 STATE_QUESTIONS 键不存在,则使用默认的错误消息
question_to_ask = session_manager.get_state(
STATE_QUESTIONS,
"Could you please provide clarification? (Error: Question not found in state)"
)
# 直接在运行 Agent 的控制台中提示用户
print("--- CONSOLE INPUT REQUIRED ---") # 打印提示,表明需要用户输入
human_reply = input(f"{question_to_ask}: ") # 显示问题并等待用户输入
print("--- CONSOLE INPUT RECEIVED ---") # 打印提示,表明已接收到用户输入
# 返回接收到的用户输入,封装在字典中
return {"reply": human_reply}
# --- 文件系统工具 ---
def get_target_directory_from_state() -> str:
"""从会话状态中获取目标目录。
Returns:
str: 目标目录路径,如果未设置则返回 "." (当前目录)。
"""
# 从共享状态中获取 STATE_TARGET_DIRECTORY 的值,如果不存在则返回 "."
return session_manager.get_state(STATE_TARGET_DIRECTORY, ".")
def get_project_structure(directory: str = None) -> Dict[str, Any]:
"""扫描目录并递归返回其结构。
Args:
directory: 要扫描的目录。如果为 None 或空字符串,则使用 "."。
Returns:
dict: 项目结构的字典表示。
"""
# 处理 directory 参数的默认值
if directory is None or directory == "":
directory = "."
# 初始化结构字典,包含文件列表和子目录字典
structure = {"files": [], "directories": {}}
try:
# 列出目录中的所有条目
items = os.listdir(directory)
for item in items:
# 构建条目的完整路径
item_path = os.path.join(directory, item)
# 判断条目是文件还是目录
if os.path.isfile(item_path):
# 如果是文件,添加到文件列表中
structure["files"].append(item)
# 如果是目录且不是隐藏目录(不以 "." 开头)
elif os.path.isdir(item_path) and not item.startswith("."):
# 递归调用自身扫描子目录,并将结果存储在 directories 字典中
structure["directories"][item] = get_project_structure(item_path)
# 返回构建好的目录结构
return structure
except Exception as e:
# 捕获异常并记录错误日志
logger.error(f"Error getting project structure for {directory}: {str(e)}")
# 返回包含错误信息的字典
return {"error": str(e)}
def scan_project_structure() -> Dict[str, Any]:
"""扫描目标目录的项目结构。
使用会话状态中的目标目录。
Returns:
dict: 项目结构的字典表示。
"""
# 获取目标目录
target_dir = get_target_directory_from_state()
# 调用 get_project_structure 扫描目标目录
return get_project_structure(target_dir)
def set_target_directory(directory: str) -> Dict[str, str]:
"""设置用于代码分析的目标目录。
Args:
directory: 要分析的目录路径。
Returns:
dict: 确认消息字典。
"""
# 将目标目录设置到共享状态中
result = session_manager.set_state(STATE_TARGET_DIRECTORY, directory)
# 记录信息日志
logger.info(f"Target directory set to: {directory}")
# 返回操作结果信息
return {
"status": "success",
"message": f"Set target directory to: {directory}",
"key": STATE_TARGET_DIRECTORY,
"directory": directory
}
def list_directory_contents(path: str = ".", include_hidden: bool = False) -> Dict[str, Any]:
"""列出目录内容并提供详细信息。
此函数提供目录内容的详细列表,包括文件和子目录,以及大小和类型等附加元数据。
Args:
path: 要列出的路径(相对或绝对)。如果为 None,则使用目标目录。
include_hidden: 是否包含隐藏文件/目录(默认值:False)。
Returns:
dict: 包含元数据的目录内容。
"""
try:
# 处理 path 参数的默认值
if path is None or path == "":
path = get_target_directory_from_state()
# 如果是相对路径,转换为绝对路径,相对于目标目录
if not os.path.isabs(path):
base_dir = get_target_directory_from_state()
path = os.path.join(base_dir, path)
# 检查路径是否存在
if not os.path.exists(path):
return {"error": f"Path not found: {path}"}
# 检查路径是否是目录
if not os.path.isdir(path):
return {"error": f"Path is not a directory: {path}"}
files = [] # 存储文件信息的列表
directories = [] # 存储目录信息的列表
# 扫描目录内容
for entry in os.scandir(path):
# 如果不包含隐藏文件/目录且条目名称以 "." 开头,则跳过
if not include_hidden and entry.name.startswith('.'):
continue
try:
# 获取条目的统计信息
stats = entry.stat()
# 构建条目的信息字典
info = {
"name": entry.name, # 名称
"path": entry.path, # 完整路径
"size": stats.st_size, # 大小
"modified": datetime.datetime.fromtimestamp( # 修改时间,转换为 ISO 格式的 UTC 时间
stats.st_mtime,
tz=ZoneInfo("UTC")
).isoformat(),
"created": datetime.datetime.fromtimestamp( # 创建时间,转换为 ISO 格式的 UTC 时间
stats.st_ctime,
tz=ZoneInfo("UTC")
).isoformat()
}
# 判断条目类型
if entry.is_file():
info["type"] = "file" # 类型为文件
files.append(info) # 添加到文件列表
elif entry.is_dir():
info["type"] = "directory" # 类型为目录
directories.append(info) # 添加到目录列表
except Exception as e:
# 捕获获取条目信息时的异常并记录警告日志
logger.warning(f"Error getting info for {entry.path}: {str(e)}")
# 继续处理下一个条目
continue
# 返回包含文件和目录列表以及总数的字典,按名称排序
return {
"files": sorted(files, key=lambda x: x["name"]),
"directories": sorted(directories, key=lambda x: x["name"]),
"current_path": path, # 当前路径
"total_files": len(files), # 文件总数
"total_directories": len(directories) # 目录总数
}
except Exception as e:
# 捕获列出目录时的异常并记录错误日志
logger.error(f"Error listing directory {path}: {str(e)}")
# 返回包含错误信息的字典
return {"error": f"Failed to list directory: {str(e)}"}
def read_file_content(
file_path: str,
start_line: Optional[int] = None,
end_line: Optional[int] = None
) -> Dict[str, Any]:
"""读取文件内容,可选择指定行范围。
此函数读取文件内容,可以返回整个文件或特定行范围的内容。
它包含安全检查和适当的错误处理。
Args:
file_path: 要读取的文件路径(绝对路径或相对于工作区)。
start_line: 可选的 1-based 起始行号(包含)。
end_line: 可选的 1-based 结束行号(包含)。
Returns:
dict: 文件内容和元数据。
"""
try:
# 如果是相对路径,转换为绝对路径,相对于目标目录
if not os.path.isabs(file_path):
target_dir = get_target_directory_from_state()
file_path = os.path.join(target_dir, file_path)
# 基本安全检查:检查文件是否存在
if not os.path.exists(file_path):
return {"error": f"File not found: {file_path}"}
# 基本安全检查:检查路径是否是文件
if not os.path.isfile(file_path):
return {"error": f"Path is not a file: {file_path}"}
# 读取文件内容
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines() # 按行读取文件内容
total_lines = len(lines) # 获取总行数
# 处理行范围参数的默认值
if start_line is None:
start_line = 1
if end_line is None:
end_line = total_lines
# 验证行号,确保在有效范围内 (1-based)
start_line = max(1, min(start_line, total_lines))
end_line = max(start_line, min(end_line, total_lines))
# 提取请求的行 (转换为 0-based 索引)
content = ''.join(lines[start_line - 1:end_line]) # 拼接选定行的内容
# 返回文件内容和相关元数据
return {
"content": content, # 文件内容
"line_count": total_lines, # 总行数
"start_line": start_line, # 实际读取的起始行号 (1-based)
"end_line": end_line, # 实际读取的结束行号 (1-based)
"file_path": file_path # 文件路径
}
except Exception as e:
# 捕获读取文件时的异常并记录错误日志
logger.error(f"Error reading file {file_path}: {str(e)}")
# 返回包含错误信息的字典
return {"error": f"Failed to read file: {str(e)}"}
# --- 项目分析工具 ---
def get_dependencies() -> Dict[str, Any]:
"""从 requirements.txt, package.json 等文件分析项目依赖。
使用会话状态中的目标目录。
Returns:
dict: 项目依赖及其版本的字典。
"""
# 获取目标目录
target_dir = get_target_directory_from_state()
dependencies = {} # 初始化依赖字典
# 检查是否存在 Python 的 requirements.txt 文件
req_path = os.path.join(target_dir, "requirements.txt")
if os.path.exists(req_path):
with open(req_path, "r") as file:
for line in file:
line = line.strip() # 移除行首尾空白
# 如果行不为空且不是注释行
if line and not line.startswith("#"):
# 尝试按 ">=" 分割,获取包名和版本
parts = line.split(">=")
if len(parts) > 1:
dependencies[parts[0].strip()] = parts[1].strip()
else:
# 尝试按 "==" 分割
parts = line.split("==")
if len(parts) > 1:
dependencies[parts[0].strip()] = parts[1].strip()
else:
# 如果没有版本指定,默认为 "latest"
dependencies[line] = "latest"
# 检查是否存在 Node.js 的 package.json 文件
pkg_path = os.path.join(target_dir, "package.json")
if os.path.exists(pkg_path):
try:
with open(pkg_path, "r") as file:
package_data = json.load(file) # 解析 JSON 数据
# 提取 dependencies
if "dependencies" in package_data:
for dep, version in package_data["dependencies"].items():
dependencies[dep] = version
# 提取 devDependencies
if "devDependencies" in package_data:
for dep, version in package_data["devDependencies"].items():
dependencies[dep] = version
except json.JSONDecodeError:
# 捕获 JSON 解析错误
dependencies["error"] = "Invalid package.json format"
# 返回收集到的依赖信息
return dependencies
def filter_by_gitignore() -> Dict[str, Any]:
"""根据 gitignore 规则过滤项目结构。
使用会话状态中的目标目录。
Returns:
dict: 过滤后的项目结构。
"""
try:
# 获取目标目录
target_dir = get_target_directory_from_state()
# 获取完整的项目结构
structure = get_project_structure(target_dir)
# 检查目标目录是否存在 .gitignore 文件
gitignore_path = os.path.join(target_dir, ".gitignore")
if not os.path.exists(gitignore_path):
# 如果不存在 .gitignore,则返回原始结构
return structure
# 解析 .gitignore 文件
matches = gitignore_parser.parse_gitignore(gitignore_path)
# 内部辅助函数,用于递归过滤结构
def filter_structure(struct, path=""):
filtered = {"files": [], "directories": {}} # 初始化过滤后的结构字典
# 过滤文件
for file in struct["files"]:
file_path = os.path.join(path, file) # 构建文件完整路径
# 如果文件路径不匹配 gitignore 规则 (即不被忽略)
if not matches(file_path):
filtered["files"].append(file) # 添加到过滤后的文件列表
# 过滤目录
for dir_name, dir_struct in struct["directories"].items():
dir_path = os.path.join(path, dir_name) # 构建目录完整路径
# 如果目录路径不匹配 gitignore 规则 (即不被忽略)
if not matches(dir_path):
# 递归调用自身过滤子目录,并将结果存储在过滤后的 directories 字典中
filtered["directories"][dir_name] = filter_structure(dir_struct, dir_path)
return filtered # 返回过滤后的结构
# 调用辅助函数开始过滤,从根目录开始
return filter_structure(structure)
except Exception as e:
# 捕获过滤过程中的异常并记录错误日志
logger.error(f"Error filtering by gitignore: {str(e)}")
# 返回包含错误信息的字典
return {"error": f"Error filtering by gitignore: {str(e)}"}
def apply_gitignore_filter() -> Dict[str, Any]:
"""应用 gitignore 过滤到项目结构。
Returns:
dict: 过滤后的项目结构。
"""
# 直接调用 filter_by_gitignore 函数
return filter_by_gitignore()
def search_codebase(
keywords: str,
file_pattern: str = "*.*",
context_lines: int = 15,
ignore_case: bool = True
) -> Dict[str, Any]:
"""在代码库中搜索关键词并包含周围上下文。
Args:
keywords: 搜索词(逗号分隔)或单个关键词/正则表达式模式。
file_pattern: 用于匹配要搜索文件的 glob 模式(默认值:所有文件)。
context_lines: 匹配行前后要包含的行数(默认值:15)。
ignore_case: 搜索时是否忽略大小写(默认值:True)。
Returns:
dict: 搜索结果,包含匹配项和上下文。
"""
try:
# 获取目标目录
target_dir = get_target_directory_from_state()
matches = [] # 存储匹配结果的列表
total_matches = 0 # 匹配总数计数器
# 处理关键词,如果是逗号分隔则分割成列表
if ',' in keywords:
# 按逗号分割并移除首尾空白
keyword_list = [k.strip() for k in keywords.split(',') if k.strip()]
else:
keyword_list = [keywords.strip()] # 单个关键词
# 记录搜索关键词日志
logger.info(f"Searching for keywords: {keyword_list}")
# 遍历目标目录下的所有文件
for root, _, files in os.walk(target_dir):
for file in files:
# 使用 glob 模式匹配文件名,如果不匹配则跳过
if not glob.fnmatch.fnmatch(file, file_pattern):
continue
file_path = os.path.join(root, file) # 构建文件完整路径
try:
# 打开并读取文件内容
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines() # 按行读取
# 遍历文件中的每一行
for i, line in enumerate(lines):
# 检查每个关键词
for keyword in keyword_list:
# 根据 ignore_case 判断是否忽略大小写进行匹配
if (ignore_case and keyword.lower() in line.lower()) or \
(not ignore_case and keyword in line):
# 计算上下文行范围 (0-based 索引)
start = max(0, i - context_lines)
end = min(len(lines), i + context_lines + 1)
# 获取上下文行和匹配行
context_before = ''.join(lines[start:i]).rstrip() # 匹配行之前的上下文
match_line = lines[i].rstrip() # 匹配行本身
context_after = ''.join(lines[i+1:end]).rstrip() # 匹配行之后的上下文
# 创建匹配项字典
match = {
"file_path": file_path, # 文件路径
"line_number": i + 1, # 匹配行号 (1-based)
"context_before": context_before, # 匹配行之前的上下文内容
"match_line": match_line, # 匹配行内容
"context_after": context_after, # 匹配行之后的上下文内容
"context_start": start + 1, # 上下文起始行号 (1-based)
"context_end": end, # 上下文结束行号 (1-based)
"matched_keyword": keyword # 匹配到的关键词
}
matches.append(match) # 添加到匹配列表
total_matches += 1 # 匹配总数加一
# 在当前行找到匹配后,跳出内层关键词循环,避免重复添加同一行的匹配
break
except Exception as e:
# 捕获搜索文件时的异常并记录警告日志
logger.warning(f"Error searching file {file_path}: {str(e)}")
continue # 继续处理下一个文件
# 按文件路径和行号对匹配结果进行排序
matches.sort(key=lambda x: (x["file_path"], x["line_number"]))
# 返回搜索结果字典
return {
"matches": matches, # 匹配项列表
"total_matches": total_matches, # 匹配总数
"search_terms": keyword_list, # 实际搜索的关键词列表
"file_pattern": file_pattern, # 使用的文件模式
"context_lines": context_lines # 使用的上下文行数
}
except Exception as e:
# 捕获整个搜索过程中的异常并记录错误日志
logger.error(f"Error during codebase search: {str(e)}")
# 返回包含错误信息的字典
return {"error": f"Failed to search codebase: {str(e)}"}
# --- Agent 协助工具 ---
def search_code_with_prompt() -> Dict[str, Any]:
"""使用会话状态中的提示词搜索代码。
这是一个占位符,理想情况下应该用更具体的逻辑实现。
Returns:
dict: 文件和匹配行的字典。
"""
# 返回一个消息,表明该功能未实现
return {"message": "NOT IMPLEMENTED; ASK USER TO IMPLEMENT CODE SEARCH IF YOU ENCOUNTER THIS MESSAGE"}
def search_tests_with_prompt() -> Dict[str, Any]:
"""使用会话状态中的提示词搜索测试文件。
这是一个占位符,理想情况下应该用更具体的逻辑实现。
Returns:
dict: 文件和匹配行的字典。
"""
# 返回一个消息,表明该功能未实现
return {"message": "NOT IMPLEMENTED; ASK USER TO IMPLEMENT TEST SEARCH IF YOU ENCOUNTER THIS MESSAGE"}
def determine_relevance_from_prompt() -> Dict[str, Any]:
"""根据会话状态确定代码文件的相关性。
Returns:
dict: 用于确定相关性的指令。
"""
# 返回一个包含指令的消息
return {
"message": "Analyze the code and test files found based on the user's prompt. "
"Rank them by relevance and explain why they might be useful for the task."
}
def set_state(key: str, value: str) -> Dict[str, str]:
"""在会话状态中设置一个值。
Args:
key: 要设置的状态键。
value: 要存储的值。
Returns:
dict: 关于操作结果的信息。
"""
# 调用 session_manager 的 set_state 方法设置状态
result = session_manager.set_state(key, value)
# 返回操作结果信息
return {
"status": result["status"],
"message": result["message"],
"key": result["key"]
}
分析
我们一直在关注人工智能如何重塑软件开发的未来。代码生成大型语言模型(LLMs)无疑是这场变革的核心力量之一。然而,将开发者模糊的意图转化为可执行、符合项目规范的代码,远非简单地将自然语言输入模型那么直接。这其中最大的挑战在于,LLMs 缺乏对具体项目环境——包括代码结构、依赖关系、现有文件内容——的感知能力。它们是强大的文本生成器,但并非天生的软件工程师。
正是为了弥合这一差距,我们看到了像 Cursor Prompt Preprocessor 这样基于 Agent 的智能系统应运而生。这不仅仅是一段 Python 代码,它代表了一种更高级、更具韧性的人工智能应用架构模式——利用模块化的 Agent 协同工作,构建一个能够理解、推理并与外部环境(包括用户和项目文件系统)交互的复杂系统。
从“黑箱”到“流水线”:Agent 架构的崛起
传统的 LLM 应用常常是将输入直接喂给模型,然后处理输出。这种模式在任务简单时有效,但在处理需要多步骤推理、信息收集或外部交互的复杂任务时显得力不从心。Agent 架构的出现,为构建更智能、更可靠的 AI 系统提供了新的范式。它将一个复杂任务分解为多个子任务,每个子任务由一个专门的 Agent 负责。Agent 之间通过共享状态或消息传递进行协作,形成一个有向无环图(DAG)或更复杂的执行流程,即所谓的“Agent Pipeline”。
Cursor Prompt Preprocessor 的核心,正是这样一个精心设计的 Agent Pipeline,它利用 Google ADK (Agent Development Kit) 框架构建,旨在将用户初步的编程提示词,转化为一个对代码生成 LLM 而言“即食”的、富含上下文的最终提示词。
管道核心:ContextFormer 的三阶段之旅
整个预处理流程的入口是 Root Agent (PromptProcessor)。它接收用户的原始提示词和目标目录,将其存入共享状态,然后将核心任务委托给其强大的子 Agent—— context_former。一旦 context_former 完成使命,Root Agent 便会提取最终生成的上下文,并向用户呈现一个简洁的摘要。
而 context_former,作为一个 SequentialAgent,则像是一个精密的工厂流水线,按部就班地执行三个关键阶段:
阶段 1: 项目基础信息收集
这是理解“土壤”的阶段。一个顺序 Agent 负责:
ProjectStructureAgent: 扫描项目目录,构建并总结项目的文件和目录结构。这就像是绘制项目的地图。结果存入 STATE_PROJECT_STRUCTURE。
DependencyAnalysisAgent: 分析项目的依赖关系,识别使用的编程语言、框架和库。这有助于理解项目的技术栈和潜在的约束。结果存入 STATE_DEPENDENCIES。
GitignoreFilterAgent: 根据 .gitignore 文件规则过滤掉不相关的文件和目录,确保后续处理只关注核心代码。结果存入 STATE_FILTERED_STRUCTURE。
这一阶段的价值在于为后续的深度分析和代码搜索奠定了基础。LLM 在没有这些信息的情况下,很容易“臆测”文件路径或依赖,导致生成错误的代码。
阶段 2: 迭代澄清与信息获取
这是管道中最具互动性和智能性的部分,由一个 LoopAgent (clarification_and_decision_loop) 实现。它最多迭代 max_iterations 次,模拟了开发者与协作者(或智能工具)之间来回沟通以明确需求的 과정。在每一轮循环中:
ParallelSearchAgent: 这是一个 ParallelAgent,它同时启动两个子 Agent:
CodeSearchAgent: 根据用户提示词(及之前的澄清信息)在项目中搜索相关的代码文件。
TestSearchAgent: 搜索相关的测试文件。
并行执行显著提高了效率,因为代码和测试搜索通常是相互独立的任务。搜索结果分别存入 STATE_RELEVANT_CODE 和 STATE_RELEVANT_TESTS。
RelevanceDeterminationAgent: 分析搜索到的文件与用户提示词的相关性,为后续的上下文选择提供依据。结果存入 STATE_RELEVANCE_SCORES。
QuestionAskingAgent: 这是智能的核心。它综合所有已知信息(原始提示词、项目结构、依赖、搜索结果、问答历史),判断当前提示词是否足够清晰。如果存在歧义或缺失信息,它会生成需要向用户提出的澄清问题。如果提示词已明确,它会输出一个特定标记 (NO_QUESTIONS)。结果存入 STATE_QUESTIONS。
UserAnswerCollectionAgent: 检查 STATE_QUESTIONS。如果存在问题,它会调用外部工具(很可能是与用户交互的界面工具)获取用户的答案,并将答案添加到 STATE_ANSWERS 列表中。如果检测到 NO_QUESTIONS 标记,它会输出一个特定的字符串 (NO_CLARIFICATION_NEEDED_EXIT_LOOP),信号 LoopAgent 终止循环。
这个循环阶段的精妙之处在于其迭代性和适应性。它允许系统在理解用户意图的过程中主动寻求帮助,而不是一次性失败。通过与用户的交互,系统能够收集到 LLM 无法凭空获得的、至关重要的上下文信息。
阶段 3: 最终上下文合成
在澄清循环结束后,由 context_formation_agent 负责收尾。它就像一个编辑,收集共享状态中的所有相关片段:原始提示词、项目结构总结、依赖信息、搜索到的相关代码和测试片段、以及完整的问答历史。它将这些信息结构化、整合,生成一个全面、准确的最终上下文对象,并将其存入 STATE_FINAL_CONTEXT。这个最终上下文包含了代码生成 LLM 所需的一切背景信息,极大地提高了生成代码的质量和相关性。
Agent 协作的基石:共享状态与工具调用
这个 Agent 系统之所以能够协同工作,关键在于其协作机制:
共享状态 (Shared State): Agent 之间不通过复杂的函数调用或对象传递来交换信息,而是通过读写一个全局的共享状态字典。每个 Agent 负责读取其所需的输入状态,执行任务,然后将结果写入特定的输出状态键。这种模式降低了 Agent 之间的耦合度,使得系统更易于扩展和维护。STATE_* 常量定义了不同 Agent 读写的“数据通道”。
Agent Pipeline (执行图): SequentialAgent, ParallelAgent, LoopAgent 这些 ADK 提供的 Agent 类型,定义了 Agent 的执行顺序和并发逻辑。它们构成了整个系统的执行图,确保了任务按照预定的流程进行。
工具使用 (Tool Usage): Agent 的智能体(通常由一个小型 LLM 驱动)根据其指令和当前共享状态,决定何时调用外部工具来执行具体操作。例如,UserAnswerCollectionAgent 调用工具与用户交互,而底层的搜索 Agent 可能调用文件系统或代码索引工具。工具是 Agent 系统与外部世界交互的“手脚”。
技术应用解读与未来展望
这种基于 Agent 的编程提示词预处理管道,在技术应用层面具有深远的意义:
提升代码生成精度: 通过提供准确的项目结构、依赖和相关代码上下文,极大地减少了 LLM 生成代码时的“幻觉”和错误,提高了生成代码的可用性。
处理复杂项目: 使得 AI 辅助代码生成能够扩展到大型、复杂的现有代码库,而不仅仅是生成独立的代码片段。
实现智能交互: 迭代澄清循环是实现真正智能、有益的 AI 协作者的关键。它使得系统能够主动识别其知识盲区,并通过与用户的交互来弥补。
模块化与可扩展性: Agent 架构的模块化特性意味着可以相对容易地添加新的 Agent 来处理额外的任务(例如,代码风格检查 Agent,安全漏洞扫描 Agent),从而增强整个系统的能力。
韧性与错误处理: 通过将复杂任务分解,系统更容易识别和隔离错误。LoopAgent 的设计也使得系统能够从不确定性中恢复,通过多轮交互最终达成目标。
我们可以预见这种 Agent Pipeline 模式将在软件开发的更多环节得到应用,例如自动化的代码审查、智能化的 bug 修复、自动化的文档生成等。挑战依然存在,例如如何更高效地管理 Agent 间的状态,如何设计更鲁棒的工具调用机制,以及如何优化 Agent 的决策过程以减少不必要的交互。
Cursor Prompt Preprocessor 提供了一个引人注目的案例,展示了如何利用 Agent 架构构建一个能够理解复杂任务、与环境交互并协同工作的智能系统,为下一代 AI 辅助编程工具奠定了基础。这不仅仅是关于生成代码,更是关于构建能够真正理解和参与软件开发过程的智能体。