agentpool 2.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of agentpool might be problematic. Click here for more details.
- acp/README.md +64 -0
- acp/__init__.py +172 -0
- acp/__main__.py +10 -0
- acp/acp_requests.py +285 -0
- acp/agent/__init__.py +6 -0
- acp/agent/connection.py +256 -0
- acp/agent/implementations/__init__.py +6 -0
- acp/agent/implementations/debug_server/__init__.py +1 -0
- acp/agent/implementations/debug_server/cli.py +79 -0
- acp/agent/implementations/debug_server/debug.html +234 -0
- acp/agent/implementations/debug_server/debug_server.py +496 -0
- acp/agent/implementations/testing.py +91 -0
- acp/agent/protocol.py +65 -0
- acp/bridge/README.md +162 -0
- acp/bridge/__init__.py +6 -0
- acp/bridge/__main__.py +91 -0
- acp/bridge/bridge.py +246 -0
- acp/bridge/py.typed +0 -0
- acp/bridge/settings.py +15 -0
- acp/client/__init__.py +7 -0
- acp/client/connection.py +251 -0
- acp/client/implementations/__init__.py +7 -0
- acp/client/implementations/default_client.py +185 -0
- acp/client/implementations/headless_client.py +266 -0
- acp/client/implementations/noop_client.py +110 -0
- acp/client/protocol.py +61 -0
- acp/connection.py +280 -0
- acp/exceptions.py +46 -0
- acp/filesystem.py +524 -0
- acp/notifications.py +832 -0
- acp/py.typed +0 -0
- acp/schema/__init__.py +265 -0
- acp/schema/agent_plan.py +30 -0
- acp/schema/agent_requests.py +126 -0
- acp/schema/agent_responses.py +256 -0
- acp/schema/base.py +39 -0
- acp/schema/capabilities.py +230 -0
- acp/schema/client_requests.py +247 -0
- acp/schema/client_responses.py +96 -0
- acp/schema/common.py +81 -0
- acp/schema/content_blocks.py +188 -0
- acp/schema/mcp.py +82 -0
- acp/schema/messages.py +171 -0
- acp/schema/notifications.py +82 -0
- acp/schema/protocol_stuff.md +3 -0
- acp/schema/session_state.py +160 -0
- acp/schema/session_updates.py +419 -0
- acp/schema/slash_commands.py +51 -0
- acp/schema/terminal.py +15 -0
- acp/schema/tool_call.py +347 -0
- acp/stdio.py +250 -0
- acp/task/__init__.py +53 -0
- acp/task/debug.py +197 -0
- acp/task/dispatcher.py +93 -0
- acp/task/queue.py +69 -0
- acp/task/sender.py +82 -0
- acp/task/state.py +87 -0
- acp/task/supervisor.py +93 -0
- acp/terminal_handle.py +30 -0
- acp/tool_call_reporter.py +199 -0
- acp/tool_call_state.py +178 -0
- acp/transports.py +104 -0
- acp/utils.py +240 -0
- agentpool/__init__.py +63 -0
- agentpool/__main__.py +7 -0
- agentpool/agents/__init__.py +30 -0
- agentpool/agents/acp_agent/__init__.py +5 -0
- agentpool/agents/acp_agent/acp_agent.py +837 -0
- agentpool/agents/acp_agent/acp_converters.py +294 -0
- agentpool/agents/acp_agent/client_handler.py +317 -0
- agentpool/agents/acp_agent/session_state.py +44 -0
- agentpool/agents/agent.py +1264 -0
- agentpool/agents/agui_agent/__init__.py +19 -0
- agentpool/agents/agui_agent/agui_agent.py +677 -0
- agentpool/agents/agui_agent/agui_converters.py +423 -0
- agentpool/agents/agui_agent/chunk_transformer.py +204 -0
- agentpool/agents/agui_agent/event_types.py +83 -0
- agentpool/agents/agui_agent/helpers.py +192 -0
- agentpool/agents/architect.py +71 -0
- agentpool/agents/base_agent.py +177 -0
- agentpool/agents/claude_code_agent/__init__.py +11 -0
- agentpool/agents/claude_code_agent/claude_code_agent.py +1021 -0
- agentpool/agents/claude_code_agent/converters.py +243 -0
- agentpool/agents/context.py +105 -0
- agentpool/agents/events/__init__.py +61 -0
- agentpool/agents/events/builtin_handlers.py +129 -0
- agentpool/agents/events/event_emitter.py +320 -0
- agentpool/agents/events/events.py +561 -0
- agentpool/agents/events/tts_handlers.py +186 -0
- agentpool/agents/interactions.py +419 -0
- agentpool/agents/slashed_agent.py +244 -0
- agentpool/agents/sys_prompts.py +178 -0
- agentpool/agents/tool_wrapping.py +184 -0
- agentpool/base_provider.py +28 -0
- agentpool/common_types.py +226 -0
- agentpool/config_resources/__init__.py +16 -0
- agentpool/config_resources/acp_assistant.yml +24 -0
- agentpool/config_resources/agents.yml +109 -0
- agentpool/config_resources/agents_template.yml +18 -0
- agentpool/config_resources/agui_test.yml +18 -0
- agentpool/config_resources/claude_code_agent.yml +16 -0
- agentpool/config_resources/claude_style_subagent.md +30 -0
- agentpool/config_resources/external_acp_agents.yml +77 -0
- agentpool/config_resources/opencode_style_subagent.md +19 -0
- agentpool/config_resources/tts_test_agents.yml +78 -0
- agentpool/delegation/__init__.py +8 -0
- agentpool/delegation/base_team.py +504 -0
- agentpool/delegation/message_flow_tracker.py +39 -0
- agentpool/delegation/pool.py +1129 -0
- agentpool/delegation/team.py +325 -0
- agentpool/delegation/teamrun.py +343 -0
- agentpool/docs/__init__.py +5 -0
- agentpool/docs/gen_examples.py +42 -0
- agentpool/docs/utils.py +370 -0
- agentpool/functional/__init__.py +20 -0
- agentpool/functional/py.typed +0 -0
- agentpool/functional/run.py +80 -0
- agentpool/functional/structure.py +136 -0
- agentpool/hooks/__init__.py +20 -0
- agentpool/hooks/agent_hooks.py +247 -0
- agentpool/hooks/base.py +119 -0
- agentpool/hooks/callable.py +140 -0
- agentpool/hooks/command.py +180 -0
- agentpool/hooks/prompt.py +122 -0
- agentpool/jinja_filters.py +132 -0
- agentpool/log.py +224 -0
- agentpool/mcp_server/__init__.py +17 -0
- agentpool/mcp_server/client.py +429 -0
- agentpool/mcp_server/constants.py +32 -0
- agentpool/mcp_server/conversions.py +172 -0
- agentpool/mcp_server/helpers.py +47 -0
- agentpool/mcp_server/manager.py +232 -0
- agentpool/mcp_server/message_handler.py +164 -0
- agentpool/mcp_server/registries/__init__.py +1 -0
- agentpool/mcp_server/registries/official_registry_client.py +345 -0
- agentpool/mcp_server/registries/pulsemcp_client.py +88 -0
- agentpool/mcp_server/tool_bridge.py +548 -0
- agentpool/messaging/__init__.py +58 -0
- agentpool/messaging/compaction.py +928 -0
- agentpool/messaging/connection_manager.py +319 -0
- agentpool/messaging/context.py +66 -0
- agentpool/messaging/event_manager.py +426 -0
- agentpool/messaging/events.py +39 -0
- agentpool/messaging/message_container.py +209 -0
- agentpool/messaging/message_history.py +491 -0
- agentpool/messaging/messagenode.py +377 -0
- agentpool/messaging/messages.py +655 -0
- agentpool/messaging/processing.py +76 -0
- agentpool/mime_utils.py +95 -0
- agentpool/models/__init__.py +21 -0
- agentpool/models/acp_agents/__init__.py +22 -0
- agentpool/models/acp_agents/base.py +308 -0
- agentpool/models/acp_agents/mcp_capable.py +790 -0
- agentpool/models/acp_agents/non_mcp.py +842 -0
- agentpool/models/agents.py +450 -0
- agentpool/models/agui_agents.py +89 -0
- agentpool/models/claude_code_agents.py +238 -0
- agentpool/models/file_agents.py +116 -0
- agentpool/models/file_parsing.py +367 -0
- agentpool/models/manifest.py +658 -0
- agentpool/observability/__init__.py +9 -0
- agentpool/observability/observability_registry.py +97 -0
- agentpool/prompts/__init__.py +1 -0
- agentpool/prompts/base.py +27 -0
- agentpool/prompts/builtin_provider.py +75 -0
- agentpool/prompts/conversion_manager.py +95 -0
- agentpool/prompts/convert.py +96 -0
- agentpool/prompts/manager.py +204 -0
- agentpool/prompts/parts/zed.md +33 -0
- agentpool/prompts/prompts.py +581 -0
- agentpool/py.typed +0 -0
- agentpool/queries/tree-sitter-language-pack/README.md +7 -0
- agentpool/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- agentpool/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- agentpool/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- agentpool/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
- agentpool/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- agentpool/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- agentpool/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- agentpool/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- agentpool/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- agentpool/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- agentpool/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- agentpool/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- agentpool/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- agentpool/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- agentpool/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- agentpool/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- agentpool/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- agentpool/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- agentpool/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- agentpool/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- agentpool/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- agentpool/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- agentpool/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- agentpool/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- agentpool/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- agentpool/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- agentpool/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- agentpool/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- agentpool/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- agentpool/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- agentpool/queries/tree-sitter-languages/README.md +24 -0
- agentpool/queries/tree-sitter-languages/c-tags.scm +9 -0
- agentpool/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- agentpool/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- agentpool/queries/tree-sitter-languages/dart-tags.scm +91 -0
- agentpool/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- agentpool/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- agentpool/queries/tree-sitter-languages/elm-tags.scm +19 -0
- agentpool/queries/tree-sitter-languages/fortran-tags.scm +15 -0
- agentpool/queries/tree-sitter-languages/go-tags.scm +30 -0
- agentpool/queries/tree-sitter-languages/haskell-tags.scm +3 -0
- agentpool/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- agentpool/queries/tree-sitter-languages/java-tags.scm +20 -0
- agentpool/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- agentpool/queries/tree-sitter-languages/julia-tags.scm +60 -0
- agentpool/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- agentpool/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- agentpool/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- agentpool/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- agentpool/queries/tree-sitter-languages/php-tags.scm +26 -0
- agentpool/queries/tree-sitter-languages/python-tags.scm +12 -0
- agentpool/queries/tree-sitter-languages/ql-tags.scm +26 -0
- agentpool/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- agentpool/queries/tree-sitter-languages/rust-tags.scm +60 -0
- agentpool/queries/tree-sitter-languages/scala-tags.scm +65 -0
- agentpool/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- agentpool/queries/tree-sitter-languages/zig-tags.scm +3 -0
- agentpool/repomap.py +1231 -0
- agentpool/resource_providers/__init__.py +17 -0
- agentpool/resource_providers/aggregating.py +54 -0
- agentpool/resource_providers/base.py +172 -0
- agentpool/resource_providers/codemode/__init__.py +9 -0
- agentpool/resource_providers/codemode/code_executor.py +215 -0
- agentpool/resource_providers/codemode/default_prompt.py +19 -0
- agentpool/resource_providers/codemode/helpers.py +83 -0
- agentpool/resource_providers/codemode/progress_executor.py +212 -0
- agentpool/resource_providers/codemode/provider.py +150 -0
- agentpool/resource_providers/codemode/remote_mcp_execution.py +143 -0
- agentpool/resource_providers/codemode/remote_provider.py +171 -0
- agentpool/resource_providers/filtering.py +42 -0
- agentpool/resource_providers/mcp_provider.py +246 -0
- agentpool/resource_providers/plan_provider.py +196 -0
- agentpool/resource_providers/pool.py +69 -0
- agentpool/resource_providers/static.py +289 -0
- agentpool/running/__init__.py +20 -0
- agentpool/running/decorators.py +56 -0
- agentpool/running/discovery.py +101 -0
- agentpool/running/executor.py +284 -0
- agentpool/running/injection.py +111 -0
- agentpool/running/py.typed +0 -0
- agentpool/running/run_nodes.py +87 -0
- agentpool/server.py +122 -0
- agentpool/sessions/__init__.py +13 -0
- agentpool/sessions/manager.py +302 -0
- agentpool/sessions/models.py +71 -0
- agentpool/sessions/session.py +239 -0
- agentpool/sessions/store.py +163 -0
- agentpool/skills/__init__.py +5 -0
- agentpool/skills/manager.py +120 -0
- agentpool/skills/registry.py +210 -0
- agentpool/skills/skill.py +36 -0
- agentpool/storage/__init__.py +17 -0
- agentpool/storage/manager.py +419 -0
- agentpool/storage/serialization.py +136 -0
- agentpool/talk/__init__.py +13 -0
- agentpool/talk/registry.py +128 -0
- agentpool/talk/stats.py +159 -0
- agentpool/talk/talk.py +604 -0
- agentpool/tasks/__init__.py +20 -0
- agentpool/tasks/exceptions.py +25 -0
- agentpool/tasks/registry.py +33 -0
- agentpool/testing.py +129 -0
- agentpool/text_templates/__init__.py +39 -0
- agentpool/text_templates/system_prompt.jinja +30 -0
- agentpool/text_templates/tool_call_default.jinja +13 -0
- agentpool/text_templates/tool_call_markdown.jinja +25 -0
- agentpool/text_templates/tool_call_simple.jinja +5 -0
- agentpool/tools/__init__.py +16 -0
- agentpool/tools/base.py +269 -0
- agentpool/tools/exceptions.py +9 -0
- agentpool/tools/manager.py +255 -0
- agentpool/tools/tool_call_info.py +87 -0
- agentpool/ui/__init__.py +2 -0
- agentpool/ui/base.py +89 -0
- agentpool/ui/mock_provider.py +81 -0
- agentpool/ui/stdlib_provider.py +150 -0
- agentpool/utils/__init__.py +44 -0
- agentpool/utils/baseregistry.py +185 -0
- agentpool/utils/count_tokens.py +62 -0
- agentpool/utils/dag.py +184 -0
- agentpool/utils/importing.py +206 -0
- agentpool/utils/inspection.py +334 -0
- agentpool/utils/model_capabilities.py +25 -0
- agentpool/utils/network.py +28 -0
- agentpool/utils/now.py +22 -0
- agentpool/utils/parse_time.py +87 -0
- agentpool/utils/result_utils.py +35 -0
- agentpool/utils/signatures.py +305 -0
- agentpool/utils/streams.py +112 -0
- agentpool/utils/tasks.py +186 -0
- agentpool/vfs_registry.py +250 -0
- agentpool-2.1.9.dist-info/METADATA +336 -0
- agentpool-2.1.9.dist-info/RECORD +474 -0
- agentpool-2.1.9.dist-info/WHEEL +4 -0
- agentpool-2.1.9.dist-info/entry_points.txt +14 -0
- agentpool-2.1.9.dist-info/licenses/LICENSE +22 -0
- agentpool_cli/__init__.py +34 -0
- agentpool_cli/__main__.py +66 -0
- agentpool_cli/agent.py +175 -0
- agentpool_cli/cli_types.py +23 -0
- agentpool_cli/common.py +163 -0
- agentpool_cli/create.py +175 -0
- agentpool_cli/history.py +217 -0
- agentpool_cli/log.py +78 -0
- agentpool_cli/py.typed +0 -0
- agentpool_cli/run.py +84 -0
- agentpool_cli/serve_acp.py +177 -0
- agentpool_cli/serve_api.py +69 -0
- agentpool_cli/serve_mcp.py +74 -0
- agentpool_cli/serve_vercel.py +233 -0
- agentpool_cli/store.py +171 -0
- agentpool_cli/task.py +84 -0
- agentpool_cli/utils.py +104 -0
- agentpool_cli/watch.py +54 -0
- agentpool_commands/__init__.py +180 -0
- agentpool_commands/agents.py +199 -0
- agentpool_commands/base.py +45 -0
- agentpool_commands/commands.py +58 -0
- agentpool_commands/completers.py +110 -0
- agentpool_commands/connections.py +175 -0
- agentpool_commands/markdown_utils.py +31 -0
- agentpool_commands/models.py +62 -0
- agentpool_commands/prompts.py +78 -0
- agentpool_commands/py.typed +0 -0
- agentpool_commands/read.py +77 -0
- agentpool_commands/resources.py +210 -0
- agentpool_commands/session.py +48 -0
- agentpool_commands/tools.py +269 -0
- agentpool_commands/utils.py +189 -0
- agentpool_commands/workers.py +163 -0
- agentpool_config/__init__.py +53 -0
- agentpool_config/builtin_tools.py +265 -0
- agentpool_config/commands.py +237 -0
- agentpool_config/conditions.py +301 -0
- agentpool_config/converters.py +30 -0
- agentpool_config/durable.py +331 -0
- agentpool_config/event_handlers.py +600 -0
- agentpool_config/events.py +153 -0
- agentpool_config/forward_targets.py +251 -0
- agentpool_config/hook_conditions.py +331 -0
- agentpool_config/hooks.py +241 -0
- agentpool_config/jinja.py +206 -0
- agentpool_config/knowledge.py +41 -0
- agentpool_config/loaders.py +350 -0
- agentpool_config/mcp_server.py +243 -0
- agentpool_config/nodes.py +202 -0
- agentpool_config/observability.py +191 -0
- agentpool_config/output_types.py +55 -0
- agentpool_config/pool_server.py +267 -0
- agentpool_config/prompt_hubs.py +105 -0
- agentpool_config/prompts.py +185 -0
- agentpool_config/py.typed +0 -0
- agentpool_config/resources.py +33 -0
- agentpool_config/session.py +119 -0
- agentpool_config/skills.py +17 -0
- agentpool_config/storage.py +288 -0
- agentpool_config/system_prompts.py +190 -0
- agentpool_config/task.py +162 -0
- agentpool_config/teams.py +52 -0
- agentpool_config/tools.py +112 -0
- agentpool_config/toolsets.py +1033 -0
- agentpool_config/workers.py +86 -0
- agentpool_prompts/__init__.py +1 -0
- agentpool_prompts/braintrust_hub.py +235 -0
- agentpool_prompts/fabric.py +75 -0
- agentpool_prompts/langfuse_hub.py +79 -0
- agentpool_prompts/promptlayer_provider.py +59 -0
- agentpool_prompts/py.typed +0 -0
- agentpool_server/__init__.py +9 -0
- agentpool_server/a2a_server/__init__.py +5 -0
- agentpool_server/a2a_server/a2a_types.py +41 -0
- agentpool_server/a2a_server/server.py +190 -0
- agentpool_server/a2a_server/storage.py +81 -0
- agentpool_server/acp_server/__init__.py +22 -0
- agentpool_server/acp_server/acp_agent.py +786 -0
- agentpool_server/acp_server/acp_tools.py +43 -0
- agentpool_server/acp_server/commands/__init__.py +18 -0
- agentpool_server/acp_server/commands/acp_commands.py +594 -0
- agentpool_server/acp_server/commands/debug_commands.py +376 -0
- agentpool_server/acp_server/commands/docs_commands/__init__.py +39 -0
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +169 -0
- agentpool_server/acp_server/commands/docs_commands/get_schema.py +176 -0
- agentpool_server/acp_server/commands/docs_commands/get_source.py +110 -0
- agentpool_server/acp_server/commands/docs_commands/git_diff.py +111 -0
- agentpool_server/acp_server/commands/docs_commands/helpers.py +33 -0
- agentpool_server/acp_server/commands/docs_commands/url_to_markdown.py +90 -0
- agentpool_server/acp_server/commands/spawn.py +210 -0
- agentpool_server/acp_server/converters.py +235 -0
- agentpool_server/acp_server/input_provider.py +338 -0
- agentpool_server/acp_server/server.py +288 -0
- agentpool_server/acp_server/session.py +969 -0
- agentpool_server/acp_server/session_manager.py +313 -0
- agentpool_server/acp_server/syntax_detection.py +250 -0
- agentpool_server/acp_server/zed_tools.md +90 -0
- agentpool_server/aggregating_server.py +309 -0
- agentpool_server/agui_server/__init__.py +11 -0
- agentpool_server/agui_server/server.py +128 -0
- agentpool_server/base.py +189 -0
- agentpool_server/http_server.py +164 -0
- agentpool_server/mcp_server/__init__.py +6 -0
- agentpool_server/mcp_server/server.py +314 -0
- agentpool_server/mcp_server/zed_wrapper.py +110 -0
- agentpool_server/openai_api_server/__init__.py +5 -0
- agentpool_server/openai_api_server/completions/__init__.py +1 -0
- agentpool_server/openai_api_server/completions/helpers.py +81 -0
- agentpool_server/openai_api_server/completions/models.py +98 -0
- agentpool_server/openai_api_server/responses/__init__.py +1 -0
- agentpool_server/openai_api_server/responses/helpers.py +74 -0
- agentpool_server/openai_api_server/responses/models.py +96 -0
- agentpool_server/openai_api_server/server.py +242 -0
- agentpool_server/py.typed +0 -0
- agentpool_storage/__init__.py +9 -0
- agentpool_storage/base.py +310 -0
- agentpool_storage/file_provider.py +378 -0
- agentpool_storage/formatters.py +129 -0
- agentpool_storage/memory_provider.py +396 -0
- agentpool_storage/models.py +108 -0
- agentpool_storage/py.typed +0 -0
- agentpool_storage/session_store.py +262 -0
- agentpool_storage/sql_provider/__init__.py +21 -0
- agentpool_storage/sql_provider/cli.py +146 -0
- agentpool_storage/sql_provider/models.py +249 -0
- agentpool_storage/sql_provider/queries.py +15 -0
- agentpool_storage/sql_provider/sql_provider.py +444 -0
- agentpool_storage/sql_provider/utils.py +234 -0
- agentpool_storage/text_log_provider.py +275 -0
- agentpool_toolsets/__init__.py +15 -0
- agentpool_toolsets/builtin/__init__.py +33 -0
- agentpool_toolsets/builtin/agent_management.py +239 -0
- agentpool_toolsets/builtin/chain.py +288 -0
- agentpool_toolsets/builtin/code.py +398 -0
- agentpool_toolsets/builtin/debug.py +291 -0
- agentpool_toolsets/builtin/execution_environment.py +381 -0
- agentpool_toolsets/builtin/file_edit/__init__.py +11 -0
- agentpool_toolsets/builtin/file_edit/file_edit.py +747 -0
- agentpool_toolsets/builtin/file_edit/fuzzy_matcher/__init__.py +5 -0
- agentpool_toolsets/builtin/file_edit/fuzzy_matcher/example_usage.py +311 -0
- agentpool_toolsets/builtin/file_edit/fuzzy_matcher/streaming_fuzzy_matcher.py +443 -0
- agentpool_toolsets/builtin/history.py +36 -0
- agentpool_toolsets/builtin/integration.py +85 -0
- agentpool_toolsets/builtin/skills.py +77 -0
- agentpool_toolsets/builtin/subagent_tools.py +324 -0
- agentpool_toolsets/builtin/tool_management.py +90 -0
- agentpool_toolsets/builtin/user_interaction.py +52 -0
- agentpool_toolsets/builtin/workers.py +128 -0
- agentpool_toolsets/composio_toolset.py +96 -0
- agentpool_toolsets/config_creation.py +192 -0
- agentpool_toolsets/entry_points.py +47 -0
- agentpool_toolsets/fsspec_toolset/__init__.py +7 -0
- agentpool_toolsets/fsspec_toolset/diagnostics.py +115 -0
- agentpool_toolsets/fsspec_toolset/grep.py +450 -0
- agentpool_toolsets/fsspec_toolset/helpers.py +631 -0
- agentpool_toolsets/fsspec_toolset/streaming_diff_parser.py +249 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +1384 -0
- agentpool_toolsets/mcp_run_toolset.py +61 -0
- agentpool_toolsets/notifications.py +146 -0
- agentpool_toolsets/openapi.py +118 -0
- agentpool_toolsets/py.typed +0 -0
- agentpool_toolsets/search_toolset.py +202 -0
- agentpool_toolsets/semantic_memory_toolset.py +536 -0
- agentpool_toolsets/streaming_tools.py +265 -0
- agentpool_toolsets/vfs_toolset.py +124 -0
agentpool/repomap.py
ADDED
|
@@ -0,0 +1,1231 @@
|
|
|
1
|
+
"""Repository map generation using tree-sitter for code analysis.
|
|
2
|
+
|
|
3
|
+
Adapted from aider's repomap module with full type annotations.
|
|
4
|
+
Uses async fsspec filesystem for non-blocking IO operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import Counter, defaultdict
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
import colorsys
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from importlib import resources
|
|
14
|
+
import math
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path, PurePosixPath
|
|
17
|
+
import random
|
|
18
|
+
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, cast
|
|
19
|
+
|
|
20
|
+
import anyio
|
|
21
|
+
from fsspec.asyn import AsyncFileSystem
|
|
22
|
+
from upathtools import is_directory
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import AsyncIterator, Sequence
|
|
27
|
+
|
|
28
|
+
from fsspec import AbstractFileSystem
|
|
29
|
+
import rustworkx as rx
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Important files that should be prioritized in repo map
|
|
33
|
+
ROOT_IMPORTANT_FILES: list[str] = [
|
|
34
|
+
"requirements.txt",
|
|
35
|
+
"setup.py",
|
|
36
|
+
"pyproject.toml",
|
|
37
|
+
"package.json",
|
|
38
|
+
"Cargo.toml",
|
|
39
|
+
"go.mod",
|
|
40
|
+
"build.gradle",
|
|
41
|
+
"pom.xml",
|
|
42
|
+
"Makefile",
|
|
43
|
+
"CMakeLists.txt",
|
|
44
|
+
"Gemfile",
|
|
45
|
+
"composer.json",
|
|
46
|
+
".env.example",
|
|
47
|
+
"Dockerfile",
|
|
48
|
+
"docker-compose.yml",
|
|
49
|
+
"README.md",
|
|
50
|
+
"README.rst",
|
|
51
|
+
"README",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
NORMALIZED_ROOT_IMPORTANT_FILES: set[str] = set(ROOT_IMPORTANT_FILES)
|
|
55
|
+
|
|
56
|
+
# Type aliases
|
|
57
|
+
type TokenCounter = Callable[[str], int]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Tag(NamedTuple):
|
|
61
|
+
"""Represents a code tag (definition or reference)."""
|
|
62
|
+
|
|
63
|
+
rel_fname: str
|
|
64
|
+
fname: str
|
|
65
|
+
line: int
|
|
66
|
+
name: str
|
|
67
|
+
kind: str
|
|
68
|
+
end_line: int = -1
|
|
69
|
+
signature_end_line: int = -1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
type RankedTag = Tag | tuple[str]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class RepoMapResult:
|
|
77
|
+
"""Result of repository map generation with metadata."""
|
|
78
|
+
|
|
79
|
+
content: str
|
|
80
|
+
total_files_processed: int
|
|
81
|
+
total_tags_found: int
|
|
82
|
+
total_files_with_tags: int
|
|
83
|
+
included_files: int
|
|
84
|
+
included_tags: int
|
|
85
|
+
truncated: bool
|
|
86
|
+
coverage_ratio: float
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class FileInfo:
|
|
91
|
+
"""Information about a file from fsspec."""
|
|
92
|
+
|
|
93
|
+
path: str
|
|
94
|
+
size: int
|
|
95
|
+
mtime: float | None = None
|
|
96
|
+
type: str = "file"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
CACHE_VERSION = 5
|
|
100
|
+
|
|
101
|
+
# Thresholds
|
|
102
|
+
MIN_TOKEN_SAMPLE_SIZE: int = 256
|
|
103
|
+
MIN_IDENT_LENGTH: int = 4
|
|
104
|
+
MAX_DEFINERS_THRESHOLD: int = 5
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_important(fname: str) -> bool:
|
|
108
|
+
"""Check if a file is considered important (like config files)."""
|
|
109
|
+
if fname in NORMALIZED_ROOT_IMPORTANT_FILES:
|
|
110
|
+
return True
|
|
111
|
+
basename = PurePosixPath(fname).name
|
|
112
|
+
return basename in NORMALIZED_ROOT_IMPORTANT_FILES
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_rel_path(path: str, root: str) -> str:
|
|
116
|
+
"""Get relative path from root."""
|
|
117
|
+
if path.startswith(root):
|
|
118
|
+
rel = path[len(root) :]
|
|
119
|
+
return rel.lstrip("/")
|
|
120
|
+
return path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class RepoMap:
|
|
124
|
+
"""Generates a map of a repository's code structure using tree-sitter.
|
|
125
|
+
|
|
126
|
+
Uses async fsspec filesystem for non-blocking IO operations.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
TAGS_CACHE_DIR: ClassVar[str] = f".agentpool.tags.cache.v{CACHE_VERSION}"
|
|
130
|
+
warned_files: ClassVar[set[str]] = set()
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
fs: AbstractFileSystem,
|
|
135
|
+
root_path: str | None = None,
|
|
136
|
+
*,
|
|
137
|
+
max_tokens: int = 1024,
|
|
138
|
+
max_line_length: int = 250,
|
|
139
|
+
token_counter: TokenCounter | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Initialize RepoMap.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
fs: Async fsspec filesystem instance.
|
|
145
|
+
root_path: Root directory path in the filesystem.
|
|
146
|
+
max_tokens: Maximum tokens for the generated map.
|
|
147
|
+
max_line_length: Maximum character length for output lines.
|
|
148
|
+
token_counter: Callable to count tokens. Defaults to len(text) / 4.
|
|
149
|
+
"""
|
|
150
|
+
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
|
|
151
|
+
|
|
152
|
+
self.fs = fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
|
|
153
|
+
self.root_path = root_path.rstrip("/") if root_path else self.fs.root_marker
|
|
154
|
+
self.max_tokens = max_tokens
|
|
155
|
+
self.max_line_length = max_line_length
|
|
156
|
+
self._token_counter = token_counter
|
|
157
|
+
|
|
158
|
+
self.tree_cache: dict[tuple[str, tuple[int, ...], float | None], str] = {}
|
|
159
|
+
self.tree_context_cache: dict[str, dict[str, Any]] = {}
|
|
160
|
+
self.TAGS_CACHE: dict[str, Any] = {}
|
|
161
|
+
|
|
162
|
+
def token_count(self, text: str) -> float:
|
|
163
|
+
"""Estimate token count for text."""
|
|
164
|
+
if self._token_counter:
|
|
165
|
+
len_text = len(text)
|
|
166
|
+
if len_text < MIN_TOKEN_SAMPLE_SIZE:
|
|
167
|
+
return self._token_counter(text)
|
|
168
|
+
|
|
169
|
+
lines = text.splitlines(keepends=True)
|
|
170
|
+
num_lines = len(lines)
|
|
171
|
+
step = num_lines // 100 or 1
|
|
172
|
+
sampled_lines = lines[::step]
|
|
173
|
+
sample_text = "".join(sampled_lines)
|
|
174
|
+
sample_tokens = self._token_counter(sample_text)
|
|
175
|
+
return sample_tokens / len(sample_text) * len_text
|
|
176
|
+
|
|
177
|
+
return len(text) / 4
|
|
178
|
+
|
|
179
|
+
async def _cat_file(self, path: str) -> str | None:
|
|
180
|
+
"""Read file content as text."""
|
|
181
|
+
try:
|
|
182
|
+
content = await self.fs._cat_file(path)
|
|
183
|
+
if isinstance(content, bytes):
|
|
184
|
+
return content.decode("utf-8")
|
|
185
|
+
except (OSError, UnicodeDecodeError):
|
|
186
|
+
return None
|
|
187
|
+
else:
|
|
188
|
+
return content # type: ignore[no-any-return]
|
|
189
|
+
|
|
190
|
+
async def _info(self, path: str) -> FileInfo | None:
|
|
191
|
+
"""Get file info."""
|
|
192
|
+
try:
|
|
193
|
+
info = await self.fs._info(path)
|
|
194
|
+
return FileInfo(
|
|
195
|
+
path=info.get("name", path),
|
|
196
|
+
size=info.get("size", 0),
|
|
197
|
+
mtime=info.get("mtime"),
|
|
198
|
+
type=info.get("type", "file"),
|
|
199
|
+
)
|
|
200
|
+
except (OSError, FileNotFoundError):
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
async def _ls(self, path: str, detail: bool = True) -> list[dict[str, Any]]:
|
|
204
|
+
"""List directory contents."""
|
|
205
|
+
try:
|
|
206
|
+
return await self.fs._ls(path, detail=detail) # type: ignore[no-any-return]
|
|
207
|
+
except (OSError, FileNotFoundError):
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
async def find_files(self, path: str, pattern: str = "**/*.py") -> list[str]:
|
|
211
|
+
"""Find files matching pattern recursively."""
|
|
212
|
+
results: list[str] = []
|
|
213
|
+
|
|
214
|
+
async def _recurse(current_path: str) -> None:
|
|
215
|
+
entries = await self._ls(current_path, detail=True)
|
|
216
|
+
for entry in entries:
|
|
217
|
+
entry_path = entry.get("name", "")
|
|
218
|
+
entry_type = entry.get("type", "")
|
|
219
|
+
|
|
220
|
+
if await is_directory(self.fs, entry_path, entry_type=entry_type):
|
|
221
|
+
await _recurse(entry_path)
|
|
222
|
+
# It's a file - process it
|
|
223
|
+
elif pattern == "**/*.py":
|
|
224
|
+
if entry_path.endswith(".py"):
|
|
225
|
+
results.append(entry_path)
|
|
226
|
+
else:
|
|
227
|
+
results.append(entry_path)
|
|
228
|
+
|
|
229
|
+
await _recurse(path)
|
|
230
|
+
return results
|
|
231
|
+
|
|
232
|
+
async def get_file_map(
|
|
233
|
+
self,
|
|
234
|
+
fname: str,
|
|
235
|
+
max_tokens: int = 2048,
|
|
236
|
+
) -> str | None:
|
|
237
|
+
"""Generate a structure map for a single file.
|
|
238
|
+
|
|
239
|
+
Unlike get_map which uses PageRank across multiple files, this method
|
|
240
|
+
shows all definitions in a single file with line numbers.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
fname: Absolute path to the file
|
|
244
|
+
max_tokens: Maximum tokens for output (approximate)
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Formatted structure map or None if no tags found
|
|
248
|
+
"""
|
|
249
|
+
rel_fname = get_rel_path(fname, self.root_path)
|
|
250
|
+
|
|
251
|
+
# Get all definition tags for this file
|
|
252
|
+
tags = await self._get_tags(fname, rel_fname)
|
|
253
|
+
def_tags = [t for t in tags if t.kind == "def"]
|
|
254
|
+
|
|
255
|
+
if not def_tags:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
# Build line ranges for rendering
|
|
259
|
+
lois: list[int] = []
|
|
260
|
+
line_ranges: dict[int, int] = {}
|
|
261
|
+
|
|
262
|
+
for tag in def_tags:
|
|
263
|
+
if tag.signature_end_line >= tag.line:
|
|
264
|
+
lois.extend(range(tag.line, tag.signature_end_line + 1))
|
|
265
|
+
else:
|
|
266
|
+
lois.append(tag.line)
|
|
267
|
+
if tag.end_line >= 0:
|
|
268
|
+
line_ranges[tag.line] = tag.end_line
|
|
269
|
+
|
|
270
|
+
# Render the tree
|
|
271
|
+
tree_output = await self._render_tree(fname, rel_fname, lois, line_ranges)
|
|
272
|
+
|
|
273
|
+
# Add header with file info
|
|
274
|
+
info = await self._info(fname)
|
|
275
|
+
size_info = f", {info.size} bytes" if info else ""
|
|
276
|
+
lines = (await self._cat_file(fname) or "").count("\n") + 1
|
|
277
|
+
tokens = self.token_count(tree_output)
|
|
278
|
+
|
|
279
|
+
header = (
|
|
280
|
+
f"# File: {rel_fname} ({lines} lines{size_info})\n"
|
|
281
|
+
f"# Structure map ({tokens} tokens). Use offset/limit to read sections.\n\n"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
result = header + f"{rel_fname}:\n" + tree_output
|
|
285
|
+
|
|
286
|
+
# Truncate if needed
|
|
287
|
+
max_chars = max_tokens * 4
|
|
288
|
+
if len(result) > max_chars:
|
|
289
|
+
result = result[:max_chars] + "\n... [truncated]\n"
|
|
290
|
+
|
|
291
|
+
return result
|
|
292
|
+
|
|
293
|
+
async def get_map(
|
|
294
|
+
self,
|
|
295
|
+
files: Sequence[str],
|
|
296
|
+
*,
|
|
297
|
+
exclude: set[str] | None = None,
|
|
298
|
+
boost_files: set[str] | None = None,
|
|
299
|
+
boost_idents: set[str] | None = None,
|
|
300
|
+
) -> str | None:
|
|
301
|
+
"""Generate a repository map for the given files.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
files: File paths to include in the map.
|
|
305
|
+
exclude: Files to exclude from the map output (but still used for ranking).
|
|
306
|
+
boost_files: Files to boost in ranking.
|
|
307
|
+
boost_idents: Identifiers to boost in ranking.
|
|
308
|
+
"""
|
|
309
|
+
if not files:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
exclude = exclude or set()
|
|
313
|
+
boost_files = boost_files or set()
|
|
314
|
+
boost_idents = boost_idents or set()
|
|
315
|
+
|
|
316
|
+
return await self._get_ranked_tags_map(
|
|
317
|
+
files=files,
|
|
318
|
+
exclude=exclude,
|
|
319
|
+
boost_files=boost_files,
|
|
320
|
+
boost_idents=boost_idents,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
async def get_map_with_metadata(
|
|
324
|
+
self,
|
|
325
|
+
files: Sequence[str],
|
|
326
|
+
*,
|
|
327
|
+
exclude: set[str] | None = None,
|
|
328
|
+
boost_files: set[str] | None = None,
|
|
329
|
+
boost_idents: set[str] | None = None,
|
|
330
|
+
) -> RepoMapResult:
|
|
331
|
+
"""Generate a repository map with detailed metadata.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
files: File paths to include in the map.
|
|
335
|
+
exclude: Files to exclude from the map output (but still used for ranking).
|
|
336
|
+
boost_files: Files to boost in ranking.
|
|
337
|
+
boost_idents: Identifiers to boost in ranking.
|
|
338
|
+
"""
|
|
339
|
+
import re
|
|
340
|
+
|
|
341
|
+
if not files:
|
|
342
|
+
return RepoMapResult(
|
|
343
|
+
content="",
|
|
344
|
+
total_files_processed=0,
|
|
345
|
+
total_tags_found=0,
|
|
346
|
+
total_files_with_tags=0,
|
|
347
|
+
included_files=0,
|
|
348
|
+
included_tags=0,
|
|
349
|
+
truncated=False,
|
|
350
|
+
coverage_ratio=0.0,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
exclude = exclude or set()
|
|
354
|
+
boost_files = boost_files or set()
|
|
355
|
+
boost_idents = boost_idents or set()
|
|
356
|
+
|
|
357
|
+
ranked_tags = await self._get_ranked_tags(
|
|
358
|
+
files=files,
|
|
359
|
+
exclude=exclude,
|
|
360
|
+
boost_files=boost_files,
|
|
361
|
+
boost_idents=boost_idents,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
total_tags = len([tag for tag in ranked_tags if isinstance(tag, Tag)])
|
|
365
|
+
all_files_with_tags = {tag.fname if isinstance(tag, Tag) else tag[0] for tag in ranked_tags}
|
|
366
|
+
total_files_with_tags = len(all_files_with_tags)
|
|
367
|
+
|
|
368
|
+
content = await self._get_ranked_tags_map(
|
|
369
|
+
files=files,
|
|
370
|
+
exclude=exclude,
|
|
371
|
+
boost_files=boost_files,
|
|
372
|
+
boost_idents=boost_idents,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if content:
|
|
376
|
+
included_files = len(set(re.findall(r"^([^:\s]+):", content, re.MULTILINE)))
|
|
377
|
+
included_tags = content.count(" def ") + content.count("class ")
|
|
378
|
+
else:
|
|
379
|
+
included_files = included_tags = 0
|
|
380
|
+
|
|
381
|
+
coverage_ratio = (
|
|
382
|
+
included_files / total_files_with_tags if total_files_with_tags > 0 else 0.0
|
|
383
|
+
)
|
|
384
|
+
truncated = included_files < total_files_with_tags or included_tags < total_tags
|
|
385
|
+
|
|
386
|
+
return RepoMapResult(
|
|
387
|
+
content=content or "",
|
|
388
|
+
total_files_processed=len(files),
|
|
389
|
+
total_tags_found=total_tags,
|
|
390
|
+
total_files_with_tags=total_files_with_tags,
|
|
391
|
+
included_files=included_files,
|
|
392
|
+
included_tags=included_tags,
|
|
393
|
+
truncated=truncated,
|
|
394
|
+
coverage_ratio=coverage_ratio,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
async def _get_tags(self, fname: str, rel_fname: str) -> list[Tag]:
|
|
398
|
+
"""Get tags for a file, using cache when possible."""
|
|
399
|
+
info = await self._info(fname)
|
|
400
|
+
if info is None:
|
|
401
|
+
return []
|
|
402
|
+
|
|
403
|
+
file_mtime = info.mtime
|
|
404
|
+
cache_key = fname
|
|
405
|
+
|
|
406
|
+
cached = self.TAGS_CACHE.get(cache_key)
|
|
407
|
+
if cached is not None and cached.get("mtime") == file_mtime:
|
|
408
|
+
return cast(list[Tag], cached["data"])
|
|
409
|
+
|
|
410
|
+
data = [tag async for tag in self._get_tags_raw(fname, rel_fname)]
|
|
411
|
+
|
|
412
|
+
self.TAGS_CACHE[cache_key] = {"mtime": file_mtime, "data": data}
|
|
413
|
+
return data
|
|
414
|
+
|
|
415
|
+
async def _get_tags_raw(self, fname: str, rel_fname: str) -> AsyncIterator[Tag]:
|
|
416
|
+
"""Extract tags from a file using tree-sitter."""
|
|
417
|
+
from grep_ast import filename_to_lang # type: ignore[import-untyped]
|
|
418
|
+
from grep_ast.tsl import get_language, get_parser # type: ignore[import-untyped]
|
|
419
|
+
from pygments.lexers import guess_lexer_for_filename
|
|
420
|
+
from pygments.token import Token
|
|
421
|
+
from tree_sitter import Query, QueryCursor
|
|
422
|
+
|
|
423
|
+
lang = filename_to_lang(fname)
|
|
424
|
+
if not lang:
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
language = get_language(lang) # pyright: ignore[reportArgumentType]
|
|
429
|
+
parser = get_parser(lang) # pyright: ignore[reportArgumentType]
|
|
430
|
+
except Exception: # noqa: BLE001
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
query_scm = get_scm_fname(lang)
|
|
434
|
+
if not query_scm or not query_scm.exists():
|
|
435
|
+
return
|
|
436
|
+
query_scm_text = query_scm.read_text("utf-8")
|
|
437
|
+
|
|
438
|
+
code = await self._cat_file(fname)
|
|
439
|
+
if not code:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
tree = parser.parse(bytes(code, "utf-8"))
|
|
443
|
+
query = Query(language, query_scm_text)
|
|
444
|
+
cursor = QueryCursor(query)
|
|
445
|
+
|
|
446
|
+
saw: set[str] = set()
|
|
447
|
+
all_nodes: list[tuple[Any, str]] = []
|
|
448
|
+
|
|
449
|
+
for _pattern_index, captures_dict in cursor.matches(tree.root_node):
|
|
450
|
+
for tag, nodes in captures_dict.items():
|
|
451
|
+
all_nodes.extend((node, tag) for node in nodes)
|
|
452
|
+
|
|
453
|
+
for node, tag in all_nodes:
|
|
454
|
+
if tag.startswith("name.definition."):
|
|
455
|
+
kind = "def"
|
|
456
|
+
elif tag.startswith("name.reference."):
|
|
457
|
+
kind = "ref"
|
|
458
|
+
else:
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
saw.add(kind)
|
|
462
|
+
name = node.text.decode("utf-8")
|
|
463
|
+
line = node.start_point[0]
|
|
464
|
+
|
|
465
|
+
end_line = -1
|
|
466
|
+
signature_end_line = -1
|
|
467
|
+
if kind == "def" and node.parent is not None:
|
|
468
|
+
end_line = node.parent.end_point[0]
|
|
469
|
+
for child in node.parent.children:
|
|
470
|
+
if child.type in ("block", "body", "compound_statement"):
|
|
471
|
+
signature_end_line = child.start_point[0] - 1
|
|
472
|
+
break
|
|
473
|
+
signature_end_line = max(signature_end_line, line)
|
|
474
|
+
|
|
475
|
+
yield Tag(
|
|
476
|
+
rel_fname=rel_fname,
|
|
477
|
+
fname=fname,
|
|
478
|
+
name=name,
|
|
479
|
+
kind=kind,
|
|
480
|
+
line=line,
|
|
481
|
+
end_line=end_line,
|
|
482
|
+
signature_end_line=signature_end_line,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if "ref" in saw or "def" in saw:
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
lexer = guess_lexer_for_filename(fname, code)
|
|
490
|
+
except Exception: # noqa: BLE001
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
tokens = list(lexer.get_tokens(code))
|
|
494
|
+
name_tokens = [token[1] for token in tokens if token[0] in Token.Name] # type: ignore[comparison-overlap]
|
|
495
|
+
|
|
496
|
+
for token in name_tokens:
|
|
497
|
+
yield Tag(rel_fname=rel_fname, fname=fname, name=token, kind="ref", line=-1)
|
|
498
|
+
|
|
499
|
+
async def _get_ranked_tags( # noqa: PLR0915
|
|
500
|
+
self,
|
|
501
|
+
files: Sequence[str],
|
|
502
|
+
exclude: set[str],
|
|
503
|
+
boost_files: set[str],
|
|
504
|
+
boost_idents: set[str],
|
|
505
|
+
) -> list[RankedTag]:
|
|
506
|
+
"""Rank tags using PageRank algorithm."""
|
|
507
|
+
import rustworkx as rx
|
|
508
|
+
|
|
509
|
+
defines: defaultdict[str, set[str]] = defaultdict(set)
|
|
510
|
+
references: defaultdict[str, list[str]] = defaultdict(list)
|
|
511
|
+
definitions: defaultdict[tuple[str, str], set[Tag]] = defaultdict(set)
|
|
512
|
+
personalization: dict[str, float] = {}
|
|
513
|
+
exclude_rel_fnames: set[str] = set()
|
|
514
|
+
|
|
515
|
+
sorted_fnames = sorted(files)
|
|
516
|
+
personalize = 100 / len(sorted_fnames) if sorted_fnames else 0
|
|
517
|
+
|
|
518
|
+
for fname in sorted_fnames:
|
|
519
|
+
info = await self._info(fname)
|
|
520
|
+
if info is None or info.type != "file":
|
|
521
|
+
if fname not in self.warned_files:
|
|
522
|
+
self.warned_files.add(fname)
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
rel_fname = get_rel_path(fname, self.root_path)
|
|
526
|
+
current_pers = 0.0
|
|
527
|
+
|
|
528
|
+
if fname in exclude:
|
|
529
|
+
current_pers += personalize
|
|
530
|
+
exclude_rel_fnames.add(rel_fname)
|
|
531
|
+
|
|
532
|
+
if fname in boost_files or rel_fname in boost_files:
|
|
533
|
+
current_pers = max(current_pers, personalize)
|
|
534
|
+
|
|
535
|
+
path_obj = PurePosixPath(rel_fname)
|
|
536
|
+
path_components = set(path_obj.parts)
|
|
537
|
+
basename_with_ext = path_obj.name
|
|
538
|
+
basename_without_ext = path_obj.stem
|
|
539
|
+
components_to_check = path_components.union({basename_with_ext, basename_without_ext})
|
|
540
|
+
|
|
541
|
+
if components_to_check.intersection(boost_idents):
|
|
542
|
+
current_pers += personalize
|
|
543
|
+
|
|
544
|
+
if current_pers > 0:
|
|
545
|
+
personalization[rel_fname] = current_pers
|
|
546
|
+
|
|
547
|
+
tags = await self._get_tags(fname, rel_fname)
|
|
548
|
+
for tag in tags:
|
|
549
|
+
if tag.kind == "def":
|
|
550
|
+
defines[tag.name].add(rel_fname)
|
|
551
|
+
key = (rel_fname, tag.name)
|
|
552
|
+
definitions[key].add(tag)
|
|
553
|
+
elif tag.kind == "ref":
|
|
554
|
+
references[tag.name].append(rel_fname)
|
|
555
|
+
|
|
556
|
+
if not references:
|
|
557
|
+
references = defaultdict(list, {k: list(v) for k, v in defines.items()})
|
|
558
|
+
|
|
559
|
+
idents = set(defines.keys()).intersection(set(references.keys()))
|
|
560
|
+
|
|
561
|
+
graph: rx.PyDiGraph[str, dict[str, Any]] = rx.PyDiGraph(multigraph=True)
|
|
562
|
+
node_to_idx: dict[str, int] = {}
|
|
563
|
+
idx_to_node: dict[int, str] = {}
|
|
564
|
+
|
|
565
|
+
def get_or_add_node(name: str) -> int:
|
|
566
|
+
if name not in node_to_idx:
|
|
567
|
+
idx = graph.add_node(name)
|
|
568
|
+
node_to_idx[name] = idx
|
|
569
|
+
idx_to_node[idx] = name
|
|
570
|
+
return node_to_idx[name]
|
|
571
|
+
|
|
572
|
+
for ident in defines:
|
|
573
|
+
if ident in references:
|
|
574
|
+
continue
|
|
575
|
+
for definer in defines[ident]:
|
|
576
|
+
idx = get_or_add_node(definer)
|
|
577
|
+
graph.add_edge(idx, idx, {"weight": 0.1, "ident": ident})
|
|
578
|
+
|
|
579
|
+
for ident in idents:
|
|
580
|
+
definers = defines[ident]
|
|
581
|
+
mul = 1.0
|
|
582
|
+
|
|
583
|
+
is_snake = ("_" in ident) and any(c.isalpha() for c in ident)
|
|
584
|
+
is_kebab = ("-" in ident) and any(c.isalpha() for c in ident)
|
|
585
|
+
is_camel = any(c.isupper() for c in ident) and any(c.islower() for c in ident)
|
|
586
|
+
|
|
587
|
+
if ident in boost_idents:
|
|
588
|
+
mul *= 10
|
|
589
|
+
if (is_snake or is_kebab or is_camel) and len(ident) >= MIN_IDENT_LENGTH:
|
|
590
|
+
mul *= 10
|
|
591
|
+
if ident.startswith("_"):
|
|
592
|
+
mul *= 0.1
|
|
593
|
+
if len(defines[ident]) > MAX_DEFINERS_THRESHOLD:
|
|
594
|
+
mul *= 0.1
|
|
595
|
+
|
|
596
|
+
for referencer, num_refs in Counter(references[ident]).items():
|
|
597
|
+
for definer in definers:
|
|
598
|
+
use_mul = mul
|
|
599
|
+
if referencer in exclude_rel_fnames:
|
|
600
|
+
use_mul *= 50
|
|
601
|
+
scaled_refs = math.sqrt(num_refs)
|
|
602
|
+
src_idx = get_or_add_node(referencer)
|
|
603
|
+
dst_idx = get_or_add_node(definer)
|
|
604
|
+
graph.add_edge(
|
|
605
|
+
src_idx, dst_idx, {"weight": use_mul * scaled_refs, "ident": ident}
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
if not graph.num_nodes():
|
|
609
|
+
return []
|
|
610
|
+
|
|
611
|
+
pers_idx: dict[int, float] | None = None
|
|
612
|
+
if personalization:
|
|
613
|
+
pers_idx = {
|
|
614
|
+
node_to_idx[name]: val
|
|
615
|
+
for name, val in personalization.items()
|
|
616
|
+
if name in node_to_idx
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
try:
|
|
620
|
+
ranked_idx = rx.pagerank(
|
|
621
|
+
graph,
|
|
622
|
+
weight_fn=lambda e: e["weight"],
|
|
623
|
+
personalization=pers_idx,
|
|
624
|
+
dangling=pers_idx,
|
|
625
|
+
)
|
|
626
|
+
except ZeroDivisionError:
|
|
627
|
+
try:
|
|
628
|
+
ranked_idx = rx.pagerank(graph, weight_fn=lambda e: e["weight"])
|
|
629
|
+
except ZeroDivisionError:
|
|
630
|
+
return []
|
|
631
|
+
|
|
632
|
+
ranked: dict[str, float] = {idx_to_node[idx]: rank for idx, rank in ranked_idx.items()}
|
|
633
|
+
|
|
634
|
+
ranked_definitions: defaultdict[tuple[str, str], float] = defaultdict(float)
|
|
635
|
+
for src_idx in graph.node_indices():
|
|
636
|
+
src_name = idx_to_node[src_idx]
|
|
637
|
+
src_rank = ranked[src_name]
|
|
638
|
+
out_edges = graph.out_edges(src_idx)
|
|
639
|
+
total_weight = sum(edge_data["weight"] for _, _, edge_data in out_edges)
|
|
640
|
+
if total_weight == 0:
|
|
641
|
+
continue
|
|
642
|
+
for _, dst_idx, edge_data in out_edges:
|
|
643
|
+
edge_rank = src_rank * edge_data["weight"] / total_weight
|
|
644
|
+
ident = edge_data["ident"]
|
|
645
|
+
dst_name = idx_to_node[dst_idx]
|
|
646
|
+
ranked_definitions[(dst_name, ident)] += edge_rank
|
|
647
|
+
|
|
648
|
+
ranked_tags: list[RankedTag] = []
|
|
649
|
+
sorted_definitions = sorted(
|
|
650
|
+
ranked_definitions.items(), reverse=True, key=lambda x: (x[1], x[0])
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
for (fname, ident), _rank in sorted_definitions:
|
|
654
|
+
if fname in exclude_rel_fnames:
|
|
655
|
+
continue
|
|
656
|
+
ranked_tags += list(definitions.get((fname, ident), []))
|
|
657
|
+
|
|
658
|
+
rel_fnames_without_tags = {get_rel_path(fname, self.root_path) for fname in files}
|
|
659
|
+
for fname in exclude:
|
|
660
|
+
rel = get_rel_path(fname, self.root_path)
|
|
661
|
+
rel_fnames_without_tags.discard(rel)
|
|
662
|
+
|
|
663
|
+
fnames_already_included = {
|
|
664
|
+
tag.rel_fname if isinstance(tag, Tag) else tag[0] for tag in ranked_tags
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
top_rank = sorted([(rank, node) for (node, rank) in ranked.items()], reverse=True)
|
|
668
|
+
for _rank, fname in top_rank:
|
|
669
|
+
if fname in rel_fnames_without_tags:
|
|
670
|
+
rel_fnames_without_tags.remove(fname)
|
|
671
|
+
if fname not in fnames_already_included:
|
|
672
|
+
ranked_tags.append((fname,))
|
|
673
|
+
|
|
674
|
+
for fname in rel_fnames_without_tags:
|
|
675
|
+
ranked_tags.append((fname,))
|
|
676
|
+
|
|
677
|
+
return ranked_tags
|
|
678
|
+
|
|
679
|
+
async def _get_ranked_tags_map(
|
|
680
|
+
self,
|
|
681
|
+
files: Sequence[str],
|
|
682
|
+
exclude: set[str],
|
|
683
|
+
boost_files: set[str],
|
|
684
|
+
boost_idents: set[str],
|
|
685
|
+
max_tokens: int | None = None,
|
|
686
|
+
) -> str | None:
|
|
687
|
+
"""Generate ranked tags map within token budget."""
|
|
688
|
+
max_tokens = max_tokens or self.max_tokens
|
|
689
|
+
|
|
690
|
+
ranked_tags = await self._get_ranked_tags(
|
|
691
|
+
files=files,
|
|
692
|
+
exclude=exclude,
|
|
693
|
+
boost_files=boost_files,
|
|
694
|
+
boost_idents=boost_idents,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
if not ranked_tags:
|
|
698
|
+
return None
|
|
699
|
+
|
|
700
|
+
num_tags = len(ranked_tags)
|
|
701
|
+
lower_bound = 0
|
|
702
|
+
upper_bound = num_tags
|
|
703
|
+
best_tree: str | None = None
|
|
704
|
+
best_tree_tokens: float = 0
|
|
705
|
+
exclude_rel_fnames = {get_rel_path(fname, self.root_path) for fname in exclude}
|
|
706
|
+
self.tree_cache = {}
|
|
707
|
+
|
|
708
|
+
middle = min(int(max_tokens // 25), num_tags)
|
|
709
|
+
|
|
710
|
+
while lower_bound <= upper_bound:
|
|
711
|
+
tree = await self._to_tree(ranked_tags[:middle], exclude_rel_fnames)
|
|
712
|
+
num_tokens = self.token_count(tree)
|
|
713
|
+
pct_err = abs(num_tokens - max_tokens) / max_tokens if max_tokens else 0
|
|
714
|
+
ok_err = 0.15
|
|
715
|
+
|
|
716
|
+
if (num_tokens <= max_tokens and num_tokens > best_tree_tokens) or pct_err < ok_err:
|
|
717
|
+
best_tree = tree
|
|
718
|
+
best_tree_tokens = num_tokens
|
|
719
|
+
if pct_err < ok_err:
|
|
720
|
+
break
|
|
721
|
+
|
|
722
|
+
if num_tokens < max_tokens:
|
|
723
|
+
lower_bound = middle + 1
|
|
724
|
+
else:
|
|
725
|
+
upper_bound = middle - 1
|
|
726
|
+
|
|
727
|
+
middle = (lower_bound + upper_bound) // 2
|
|
728
|
+
|
|
729
|
+
return best_tree
|
|
730
|
+
|
|
731
|
+
async def _render_tree(
|
|
732
|
+
self,
|
|
733
|
+
abs_fname: str,
|
|
734
|
+
rel_fname: str,
|
|
735
|
+
lois: list[int],
|
|
736
|
+
line_ranges: dict[int, int] | None = None,
|
|
737
|
+
) -> str:
|
|
738
|
+
"""Render a tree representation of a file with lines of interest."""
|
|
739
|
+
import re
|
|
740
|
+
|
|
741
|
+
from grep_ast import TreeContext
|
|
742
|
+
|
|
743
|
+
if line_ranges is None:
|
|
744
|
+
line_ranges = {}
|
|
745
|
+
|
|
746
|
+
info = await self._info(abs_fname)
|
|
747
|
+
mtime = info.mtime if info else None
|
|
748
|
+
|
|
749
|
+
key = (rel_fname, tuple(sorted(lois)), mtime)
|
|
750
|
+
if key in self.tree_cache:
|
|
751
|
+
return self.tree_cache[key]
|
|
752
|
+
|
|
753
|
+
cached = self.tree_context_cache.get(rel_fname)
|
|
754
|
+
if cached is None or cached["mtime"] != mtime:
|
|
755
|
+
code = await self._cat_file(abs_fname) or ""
|
|
756
|
+
if not code.endswith("\n"):
|
|
757
|
+
code += "\n"
|
|
758
|
+
|
|
759
|
+
context = TreeContext(
|
|
760
|
+
rel_fname,
|
|
761
|
+
code,
|
|
762
|
+
child_context=False,
|
|
763
|
+
last_line=False,
|
|
764
|
+
margin=0,
|
|
765
|
+
mark_lois=False,
|
|
766
|
+
loi_pad=0,
|
|
767
|
+
show_top_of_file_parent_scope=False,
|
|
768
|
+
)
|
|
769
|
+
self.tree_context_cache[rel_fname] = {"context": context, "mtime": mtime}
|
|
770
|
+
|
|
771
|
+
context = self.tree_context_cache[rel_fname]["context"]
|
|
772
|
+
context.lines_of_interest = set()
|
|
773
|
+
context.add_lines_of_interest(lois)
|
|
774
|
+
context.add_context()
|
|
775
|
+
res: str = context.format()
|
|
776
|
+
|
|
777
|
+
code = await self._cat_file(abs_fname) or ""
|
|
778
|
+
code_lines = code.splitlines()
|
|
779
|
+
lois_set = set(lois)
|
|
780
|
+
|
|
781
|
+
def_pattern = re.compile(r"^(.*?)(class\s+\w+|def\s+\w+|async\s+def\s+\w+)")
|
|
782
|
+
|
|
783
|
+
result_lines = []
|
|
784
|
+
for output_line in res.splitlines():
|
|
785
|
+
modified_line = output_line
|
|
786
|
+
match = def_pattern.search(output_line)
|
|
787
|
+
if match:
|
|
788
|
+
stripped = output_line.lstrip("│ \t")
|
|
789
|
+
for line_num in lois_set:
|
|
790
|
+
if line_num < len(code_lines):
|
|
791
|
+
orig_line = code_lines[line_num].strip()
|
|
792
|
+
if orig_line and stripped.startswith(orig_line.split("(")[0].split(":")[0]):
|
|
793
|
+
name_match = re.search(
|
|
794
|
+
r"(class\s+\w+|def\s+\w+|async\s+def\s+\w+)", output_line
|
|
795
|
+
)
|
|
796
|
+
if name_match:
|
|
797
|
+
start_line_display = line_num + 1
|
|
798
|
+
end_line = line_ranges.get(line_num, -1)
|
|
799
|
+
if end_line >= 0 and end_line != line_num:
|
|
800
|
+
end_line_display = end_line + 1
|
|
801
|
+
line_info = f" # [{start_line_display}-{end_line_display}]"
|
|
802
|
+
else:
|
|
803
|
+
line_info = f" # [{start_line_display}]"
|
|
804
|
+
modified_line = f"{output_line}{line_info}"
|
|
805
|
+
break
|
|
806
|
+
result_lines.append(modified_line)
|
|
807
|
+
|
|
808
|
+
res = "\n".join(result_lines)
|
|
809
|
+
if result_lines:
|
|
810
|
+
res += "\n"
|
|
811
|
+
|
|
812
|
+
self.tree_cache[key] = res
|
|
813
|
+
return res
|
|
814
|
+
|
|
815
|
+
async def _to_tree(self, tags: list[RankedTag], exclude_rel_fnames: set[str]) -> str:
|
|
816
|
+
"""Convert ranked tags to a tree representation."""
|
|
817
|
+
if not tags:
|
|
818
|
+
return ""
|
|
819
|
+
|
|
820
|
+
cur_fname: str | None = None
|
|
821
|
+
cur_abs_fname: str | None = None
|
|
822
|
+
lois: list[int] | None = None
|
|
823
|
+
line_ranges: dict[int, int] | None = None
|
|
824
|
+
output = ""
|
|
825
|
+
|
|
826
|
+
dummy_tag: tuple[None] = (None,)
|
|
827
|
+
for tag in [*sorted(tags, key=lambda t: str(t[0]) if t[0] else ""), dummy_tag]:
|
|
828
|
+
this_rel_fname = tag[0] if isinstance(tag, Tag) else (tag[0] if tag[0] else None)
|
|
829
|
+
|
|
830
|
+
if isinstance(tag, Tag):
|
|
831
|
+
this_rel_fname = tag.rel_fname
|
|
832
|
+
elif tag[0] is not None:
|
|
833
|
+
this_rel_fname = tag[0]
|
|
834
|
+
else:
|
|
835
|
+
this_rel_fname = None
|
|
836
|
+
|
|
837
|
+
if this_rel_fname and this_rel_fname in exclude_rel_fnames:
|
|
838
|
+
continue
|
|
839
|
+
|
|
840
|
+
if this_rel_fname != cur_fname:
|
|
841
|
+
if lois is not None and cur_fname and cur_abs_fname:
|
|
842
|
+
output += "\n"
|
|
843
|
+
output += cur_fname + ":\n"
|
|
844
|
+
output += await self._render_tree(cur_abs_fname, cur_fname, lois, line_ranges)
|
|
845
|
+
lois = None
|
|
846
|
+
line_ranges = None
|
|
847
|
+
elif cur_fname:
|
|
848
|
+
output += "\n" + cur_fname + "\n"
|
|
849
|
+
|
|
850
|
+
if isinstance(tag, Tag):
|
|
851
|
+
lois = []
|
|
852
|
+
line_ranges = {}
|
|
853
|
+
cur_abs_fname = tag.fname
|
|
854
|
+
cur_fname = this_rel_fname
|
|
855
|
+
|
|
856
|
+
if lois is not None and line_ranges is not None and isinstance(tag, Tag):
|
|
857
|
+
if tag.signature_end_line >= tag.line:
|
|
858
|
+
lois.extend(range(tag.line, tag.signature_end_line + 1))
|
|
859
|
+
else:
|
|
860
|
+
lois.append(tag.line)
|
|
861
|
+
if tag.end_line >= 0:
|
|
862
|
+
line_ranges[tag.line] = tag.end_line
|
|
863
|
+
|
|
864
|
+
return "\n".join([line[: self.max_line_length] for line in output.splitlines()]) + "\n"
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def get_random_color() -> str:
|
|
868
|
+
"""Generate a random pastel color."""
|
|
869
|
+
hue = random.random()
|
|
870
|
+
r, g, b = (int(x * 255) for x in colorsys.hsv_to_rgb(hue, 1, 0.75))
|
|
871
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def get_scm_fname(lang: str) -> Path | None:
|
|
875
|
+
"""Get the path to the SCM query file for a language."""
|
|
876
|
+
package = __package__ or "agentpool"
|
|
877
|
+
subdir = "tree-sitter-language-pack"
|
|
878
|
+
try:
|
|
879
|
+
path = resources.files(package).joinpath("queries", subdir, f"{lang}-tags.scm")
|
|
880
|
+
if path.is_file():
|
|
881
|
+
return Path(str(path))
|
|
882
|
+
except KeyError:
|
|
883
|
+
pass
|
|
884
|
+
|
|
885
|
+
subdir = "tree-sitter-languages"
|
|
886
|
+
try:
|
|
887
|
+
path = resources.files(package).joinpath("queries", subdir, f"{lang}-tags.scm")
|
|
888
|
+
return Path(str(path))
|
|
889
|
+
except KeyError:
|
|
890
|
+
return None
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def get_supported_languages() -> set[str]:
|
|
894
|
+
"""Get set of languages that have tree-sitter tag support."""
|
|
895
|
+
from grep_ast.parsers import PARSERS # type: ignore[import-untyped]
|
|
896
|
+
|
|
897
|
+
supported = set()
|
|
898
|
+
for lang in set(PARSERS.values()):
|
|
899
|
+
scm = get_scm_fname(lang)
|
|
900
|
+
if scm and scm.exists():
|
|
901
|
+
supported.add(lang)
|
|
902
|
+
return supported
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def is_language_supported(fname: str) -> bool:
|
|
906
|
+
"""Check if a file's language supports tree-sitter tags."""
|
|
907
|
+
from grep_ast import filename_to_lang
|
|
908
|
+
|
|
909
|
+
lang = filename_to_lang(fname)
|
|
910
|
+
if not lang:
|
|
911
|
+
return False
|
|
912
|
+
scm = get_scm_fname(lang)
|
|
913
|
+
return scm is not None and scm.exists()
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def get_tags_from_content(content: str, filename: str) -> list[Tag]:
|
|
917
|
+
"""Extract tags from content without filesystem IO.
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
content: File content as string
|
|
921
|
+
filename: Filename for language detection (e.g. "foo.py")
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
List of Tag objects (definitions and references)
|
|
925
|
+
"""
|
|
926
|
+
from grep_ast import filename_to_lang
|
|
927
|
+
from grep_ast.tsl import get_language, get_parser
|
|
928
|
+
from pygments.lexers import guess_lexer_for_filename
|
|
929
|
+
from pygments.token import Token
|
|
930
|
+
from tree_sitter import Query, QueryCursor
|
|
931
|
+
from tree_sitter_language_pack import SupportedLanguage
|
|
932
|
+
|
|
933
|
+
lang = cast(SupportedLanguage, filename_to_lang(filename))
|
|
934
|
+
if not lang:
|
|
935
|
+
return []
|
|
936
|
+
|
|
937
|
+
try:
|
|
938
|
+
language = get_language(lang)
|
|
939
|
+
parser = get_parser(lang)
|
|
940
|
+
except Exception: # noqa: BLE001
|
|
941
|
+
return []
|
|
942
|
+
|
|
943
|
+
query_scm = get_scm_fname(lang)
|
|
944
|
+
if not query_scm or not query_scm.exists():
|
|
945
|
+
return []
|
|
946
|
+
query_scm_text = query_scm.read_text("utf-8")
|
|
947
|
+
|
|
948
|
+
tree = parser.parse(bytes(content, "utf-8"))
|
|
949
|
+
query = Query(language, query_scm_text)
|
|
950
|
+
cursor = QueryCursor(query)
|
|
951
|
+
|
|
952
|
+
tags: list[Tag] = []
|
|
953
|
+
saw: set[str] = set()
|
|
954
|
+
all_nodes: list[tuple[Any, str]] = []
|
|
955
|
+
|
|
956
|
+
for _pattern_index, captures_dict in cursor.matches(tree.root_node):
|
|
957
|
+
for tag, nodes in captures_dict.items():
|
|
958
|
+
all_nodes.extend((node, tag) for node in nodes)
|
|
959
|
+
|
|
960
|
+
for node, tag in all_nodes:
|
|
961
|
+
if tag.startswith("name.definition."):
|
|
962
|
+
kind = "def"
|
|
963
|
+
elif tag.startswith("name.reference."):
|
|
964
|
+
kind = "ref"
|
|
965
|
+
else:
|
|
966
|
+
continue
|
|
967
|
+
|
|
968
|
+
saw.add(kind)
|
|
969
|
+
name = node.text.decode("utf-8")
|
|
970
|
+
line = node.start_point[0]
|
|
971
|
+
|
|
972
|
+
end_line = -1
|
|
973
|
+
signature_end_line = -1
|
|
974
|
+
if kind == "def" and node.parent is not None:
|
|
975
|
+
end_line = node.parent.end_point[0]
|
|
976
|
+
for child in node.parent.children:
|
|
977
|
+
if child.type in ("block", "body", "compound_statement"):
|
|
978
|
+
signature_end_line = child.start_point[0] - 1
|
|
979
|
+
break
|
|
980
|
+
signature_end_line = max(signature_end_line, line)
|
|
981
|
+
|
|
982
|
+
tags.append(
|
|
983
|
+
Tag(
|
|
984
|
+
rel_fname=filename,
|
|
985
|
+
fname=filename,
|
|
986
|
+
name=name,
|
|
987
|
+
kind=kind,
|
|
988
|
+
line=line,
|
|
989
|
+
end_line=end_line,
|
|
990
|
+
signature_end_line=signature_end_line,
|
|
991
|
+
)
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
if "ref" in saw or "def" in saw:
|
|
995
|
+
return tags
|
|
996
|
+
|
|
997
|
+
# Fallback to pygments lexer for references
|
|
998
|
+
try:
|
|
999
|
+
lexer = guess_lexer_for_filename(filename, content)
|
|
1000
|
+
except Exception: # noqa: BLE001
|
|
1001
|
+
return tags
|
|
1002
|
+
|
|
1003
|
+
tokens = list(lexer.get_tokens(content))
|
|
1004
|
+
name_tokens = [token[1] for token in tokens if token[0] in Token.Name] # type: ignore[comparison-overlap]
|
|
1005
|
+
|
|
1006
|
+
for token in name_tokens:
|
|
1007
|
+
tags.append(Tag(rel_fname=filename, fname=filename, name=token, kind="ref", line=-1)) # noqa: PERF401
|
|
1008
|
+
|
|
1009
|
+
return tags
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def get_file_map_from_content( # noqa: PLR0915
|
|
1013
|
+
content: str,
|
|
1014
|
+
filename: str,
|
|
1015
|
+
max_tokens: int = 2048,
|
|
1016
|
+
) -> str | None:
|
|
1017
|
+
"""Generate structure map from content without filesystem IO.
|
|
1018
|
+
|
|
1019
|
+
Pure function for generating a file structure map when you already
|
|
1020
|
+
have the content in memory (e.g., from ACP embedded resources).
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
content: File content as string
|
|
1024
|
+
filename: Filename for language detection (e.g. "foo.py", "src/main.rs")
|
|
1025
|
+
max_tokens: Maximum tokens for output (approximate)
|
|
1026
|
+
|
|
1027
|
+
Returns:
|
|
1028
|
+
Formatted structure map or None if language not supported
|
|
1029
|
+
"""
|
|
1030
|
+
from grep_ast import TreeContext
|
|
1031
|
+
|
|
1032
|
+
if not is_language_supported(filename):
|
|
1033
|
+
return None
|
|
1034
|
+
|
|
1035
|
+
# Get definition tags
|
|
1036
|
+
tags = get_tags_from_content(content, filename)
|
|
1037
|
+
def_tags = [t for t in tags if t.kind == "def"]
|
|
1038
|
+
|
|
1039
|
+
if not def_tags:
|
|
1040
|
+
return None
|
|
1041
|
+
|
|
1042
|
+
# Build line ranges for rendering
|
|
1043
|
+
lois: list[int] = []
|
|
1044
|
+
line_ranges: dict[int, int] = {}
|
|
1045
|
+
|
|
1046
|
+
for tag in def_tags:
|
|
1047
|
+
if tag.signature_end_line >= tag.line:
|
|
1048
|
+
lois.extend(range(tag.line, tag.signature_end_line + 1))
|
|
1049
|
+
else:
|
|
1050
|
+
lois.append(tag.line)
|
|
1051
|
+
if tag.end_line >= 0:
|
|
1052
|
+
line_ranges[tag.line] = tag.end_line
|
|
1053
|
+
|
|
1054
|
+
# Render tree using TreeContext
|
|
1055
|
+
code = content if content.endswith("\n") else content + "\n"
|
|
1056
|
+
context = TreeContext(
|
|
1057
|
+
filename,
|
|
1058
|
+
code,
|
|
1059
|
+
child_context=False,
|
|
1060
|
+
last_line=False,
|
|
1061
|
+
margin=0,
|
|
1062
|
+
mark_lois=False,
|
|
1063
|
+
loi_pad=0,
|
|
1064
|
+
show_top_of_file_parent_scope=False,
|
|
1065
|
+
)
|
|
1066
|
+
context.add_lines_of_interest(lois)
|
|
1067
|
+
context.add_context()
|
|
1068
|
+
tree_output: str = context.format()
|
|
1069
|
+
|
|
1070
|
+
# Add line number annotations to definitions
|
|
1071
|
+
import re
|
|
1072
|
+
|
|
1073
|
+
code_lines = content.splitlines()
|
|
1074
|
+
lois_set = set(lois)
|
|
1075
|
+
def_pattern = re.compile(r"^(.*?)(class\s+\w+|def\s+\w+|async\s+def\s+\w+)")
|
|
1076
|
+
|
|
1077
|
+
result_lines = []
|
|
1078
|
+
for output_line in tree_output.splitlines():
|
|
1079
|
+
modified_line = output_line
|
|
1080
|
+
match = def_pattern.search(output_line)
|
|
1081
|
+
if match:
|
|
1082
|
+
stripped = output_line.lstrip("│ \t")
|
|
1083
|
+
for line_num in lois_set:
|
|
1084
|
+
if line_num < len(code_lines):
|
|
1085
|
+
orig_line = code_lines[line_num].strip()
|
|
1086
|
+
if orig_line and stripped.startswith(orig_line.split("(")[0].split(":")[0]):
|
|
1087
|
+
name_match = re.search(
|
|
1088
|
+
r"(class\s+\w+|def\s+\w+|async\s+def\s+\w+)", output_line
|
|
1089
|
+
)
|
|
1090
|
+
if name_match:
|
|
1091
|
+
start_line_display = line_num + 1
|
|
1092
|
+
end_line = line_ranges.get(line_num, -1)
|
|
1093
|
+
if end_line >= 0 and end_line != line_num:
|
|
1094
|
+
end_line_display = end_line + 1
|
|
1095
|
+
line_info = f" # [{start_line_display}-{end_line_display}]"
|
|
1096
|
+
else:
|
|
1097
|
+
line_info = f" # [{start_line_display}]"
|
|
1098
|
+
modified_line = f"{output_line}{line_info}"
|
|
1099
|
+
break
|
|
1100
|
+
result_lines.append(modified_line)
|
|
1101
|
+
|
|
1102
|
+
tree_output = "\n".join(result_lines)
|
|
1103
|
+
if result_lines:
|
|
1104
|
+
tree_output += "\n"
|
|
1105
|
+
|
|
1106
|
+
# Build final output with header
|
|
1107
|
+
lines = content.count("\n") + 1
|
|
1108
|
+
tokens_approx = len(tree_output) // 4
|
|
1109
|
+
|
|
1110
|
+
header = (
|
|
1111
|
+
f"# File: {filename} ({lines} lines)\n"
|
|
1112
|
+
f"# Structure map (~{tokens_approx} tokens). Use read_file with line/limit for details.\n\n"
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
result = header + f"{filename}:\n" + tree_output
|
|
1116
|
+
|
|
1117
|
+
# Truncate if needed
|
|
1118
|
+
max_chars = max_tokens * 4
|
|
1119
|
+
if len(result) > max_chars:
|
|
1120
|
+
result = result[:max_chars] + "\n... [truncated]\n"
|
|
1121
|
+
|
|
1122
|
+
return result
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def truncate_with_notice(
|
|
1126
|
+
path: str,
|
|
1127
|
+
content: str,
|
|
1128
|
+
head_lines: int = 100,
|
|
1129
|
+
tail_lines: int = 50,
|
|
1130
|
+
) -> str:
|
|
1131
|
+
"""Show head + tail for files where structure map isn't supported.
|
|
1132
|
+
|
|
1133
|
+
Args:
|
|
1134
|
+
path: File path for display
|
|
1135
|
+
content: Full file content
|
|
1136
|
+
head_lines: Number of lines to show from start
|
|
1137
|
+
tail_lines: Number of lines to show from end
|
|
1138
|
+
|
|
1139
|
+
Returns:
|
|
1140
|
+
Truncated content with notice about omitted lines
|
|
1141
|
+
"""
|
|
1142
|
+
lines = content.splitlines()
|
|
1143
|
+
total = len(lines)
|
|
1144
|
+
|
|
1145
|
+
if total <= head_lines + tail_lines:
|
|
1146
|
+
return content
|
|
1147
|
+
|
|
1148
|
+
head = lines[:head_lines]
|
|
1149
|
+
tail = lines[-tail_lines:]
|
|
1150
|
+
omitted = total - head_lines - tail_lines
|
|
1151
|
+
|
|
1152
|
+
return (
|
|
1153
|
+
f"# File: {path} ({total} lines)\n"
|
|
1154
|
+
f"# Showing first {head_lines} and last {tail_lines} lines "
|
|
1155
|
+
f"(language not supported for structure map)\n\n"
|
|
1156
|
+
+ "\n".join(head)
|
|
1157
|
+
+ f"\n\n... [{omitted} lines omitted] ...\n\n"
|
|
1158
|
+
+ "\n".join(tail)
|
|
1159
|
+
+ "\n\n# Use offset/limit params to read specific sections"
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def get_supported_languages_md() -> str:
|
|
1164
|
+
"""Generate markdown table of supported languages."""
|
|
1165
|
+
from grep_ast.parsers import PARSERS
|
|
1166
|
+
|
|
1167
|
+
res = "| Language | Extension | Repo Map |\n"
|
|
1168
|
+
res += "|----------|-----------|----------|\n"
|
|
1169
|
+
|
|
1170
|
+
data = sorted((lang, ext) for ext, lang in PARSERS.items())
|
|
1171
|
+
for lang, ext in data:
|
|
1172
|
+
fn = get_scm_fname(lang)
|
|
1173
|
+
repo_map = "✓" if fn and fn.exists() else ""
|
|
1174
|
+
res += f"| {lang:20} | {ext:20} | {repo_map:^8} |\n"
|
|
1175
|
+
|
|
1176
|
+
res += "\n"
|
|
1177
|
+
return res
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
async def find_src_files(fs: AbstractFileSystem, directory: str) -> list[str]:
|
|
1181
|
+
"""Find all source files in a directory using async fsspec."""
|
|
1182
|
+
results: list[str] = []
|
|
1183
|
+
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
|
|
1184
|
+
|
|
1185
|
+
fs = fs if isinstance(fs, AsyncFileSystem) else AsyncFileSystemWrapper(fs)
|
|
1186
|
+
|
|
1187
|
+
async def _recurse(path: str) -> None:
|
|
1188
|
+
try:
|
|
1189
|
+
entries = await fs._ls(path, detail=True)
|
|
1190
|
+
except (OSError, FileNotFoundError):
|
|
1191
|
+
return
|
|
1192
|
+
|
|
1193
|
+
for entry in entries:
|
|
1194
|
+
entry_path = entry.get("name", "")
|
|
1195
|
+
entry_type = entry.get("type", "")
|
|
1196
|
+
|
|
1197
|
+
if await is_directory(fs, entry_path, entry_type=entry_type):
|
|
1198
|
+
await _recurse(entry_path)
|
|
1199
|
+
else:
|
|
1200
|
+
results.append(entry_path)
|
|
1201
|
+
|
|
1202
|
+
await _recurse(directory)
|
|
1203
|
+
return results
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
if __name__ == "__main__":
|
|
1207
|
+
|
|
1208
|
+
async def main() -> None:
|
|
1209
|
+
from fsspec.implementations.github import GithubFileSystem
|
|
1210
|
+
|
|
1211
|
+
gh_token = os.environ.get("GH_TOKEN")
|
|
1212
|
+
fs = GithubFileSystem(
|
|
1213
|
+
org="phil65", repo="epregistry", sha="main", username="phil65", token=gh_token
|
|
1214
|
+
)
|
|
1215
|
+
root_path = "src/epregistry"
|
|
1216
|
+
all_py_files = [f for f in await find_src_files(fs, root_path) if f.endswith(".py")]
|
|
1217
|
+
rm = RepoMap(fs=fs, root_path=root_path, max_tokens=4000)
|
|
1218
|
+
result = await rm.get_map_with_metadata(all_py_files)
|
|
1219
|
+
print("\n" + "=" * 80)
|
|
1220
|
+
print("REPOSITORY MAP METADATA")
|
|
1221
|
+
print("=" * 80)
|
|
1222
|
+
print(result)
|
|
1223
|
+
if result.content:
|
|
1224
|
+
print("\n" + "=" * 80)
|
|
1225
|
+
print("REPOSITORY MAP")
|
|
1226
|
+
print("=" * 80)
|
|
1227
|
+
print(result.content)
|
|
1228
|
+
else:
|
|
1229
|
+
print("No repository map generated")
|
|
1230
|
+
|
|
1231
|
+
anyio.run(main)
|