PraisonAI 3.0.0__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.
- praisonai/__init__.py +54 -0
- praisonai/__main__.py +15 -0
- praisonai/acp/__init__.py +54 -0
- praisonai/acp/config.py +159 -0
- praisonai/acp/server.py +587 -0
- praisonai/acp/session.py +219 -0
- praisonai/adapters/__init__.py +50 -0
- praisonai/adapters/readers.py +395 -0
- praisonai/adapters/rerankers.py +315 -0
- praisonai/adapters/retrievers.py +394 -0
- praisonai/adapters/vector_stores.py +409 -0
- praisonai/agent_scheduler.py +337 -0
- praisonai/agents_generator.py +903 -0
- praisonai/api/call.py +292 -0
- praisonai/auto.py +1197 -0
- praisonai/capabilities/__init__.py +275 -0
- praisonai/capabilities/a2a.py +140 -0
- praisonai/capabilities/assistants.py +283 -0
- praisonai/capabilities/audio.py +320 -0
- praisonai/capabilities/batches.py +469 -0
- praisonai/capabilities/completions.py +336 -0
- praisonai/capabilities/container_files.py +155 -0
- praisonai/capabilities/containers.py +93 -0
- praisonai/capabilities/embeddings.py +158 -0
- praisonai/capabilities/files.py +467 -0
- praisonai/capabilities/fine_tuning.py +293 -0
- praisonai/capabilities/guardrails.py +182 -0
- praisonai/capabilities/images.py +330 -0
- praisonai/capabilities/mcp.py +190 -0
- praisonai/capabilities/messages.py +270 -0
- praisonai/capabilities/moderations.py +154 -0
- praisonai/capabilities/ocr.py +217 -0
- praisonai/capabilities/passthrough.py +204 -0
- praisonai/capabilities/rag.py +207 -0
- praisonai/capabilities/realtime.py +160 -0
- praisonai/capabilities/rerank.py +165 -0
- praisonai/capabilities/responses.py +266 -0
- praisonai/capabilities/search.py +109 -0
- praisonai/capabilities/skills.py +133 -0
- praisonai/capabilities/vector_store_files.py +334 -0
- praisonai/capabilities/vector_stores.py +304 -0
- praisonai/capabilities/videos.py +141 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/chat/__init__.py +106 -0
- praisonai/chat/app.py +125 -0
- praisonai/cli/__init__.py +26 -0
- praisonai/cli/app.py +213 -0
- praisonai/cli/commands/__init__.py +75 -0
- praisonai/cli/commands/acp.py +70 -0
- praisonai/cli/commands/completion.py +333 -0
- praisonai/cli/commands/config.py +166 -0
- praisonai/cli/commands/debug.py +142 -0
- praisonai/cli/commands/diag.py +55 -0
- praisonai/cli/commands/doctor.py +166 -0
- praisonai/cli/commands/environment.py +179 -0
- praisonai/cli/commands/lsp.py +112 -0
- praisonai/cli/commands/mcp.py +210 -0
- praisonai/cli/commands/profile.py +457 -0
- praisonai/cli/commands/run.py +228 -0
- praisonai/cli/commands/schedule.py +150 -0
- praisonai/cli/commands/serve.py +97 -0
- praisonai/cli/commands/session.py +212 -0
- praisonai/cli/commands/traces.py +145 -0
- praisonai/cli/commands/version.py +101 -0
- praisonai/cli/configuration/__init__.py +18 -0
- praisonai/cli/configuration/loader.py +353 -0
- praisonai/cli/configuration/paths.py +114 -0
- praisonai/cli/configuration/schema.py +164 -0
- praisonai/cli/features/__init__.py +268 -0
- praisonai/cli/features/acp.py +236 -0
- praisonai/cli/features/action_orchestrator.py +546 -0
- praisonai/cli/features/agent_scheduler.py +773 -0
- praisonai/cli/features/agent_tools.py +474 -0
- praisonai/cli/features/agents.py +375 -0
- praisonai/cli/features/at_mentions.py +471 -0
- praisonai/cli/features/auto_memory.py +182 -0
- praisonai/cli/features/autonomy_mode.py +490 -0
- praisonai/cli/features/background.py +356 -0
- praisonai/cli/features/base.py +168 -0
- praisonai/cli/features/capabilities.py +1326 -0
- praisonai/cli/features/checkpoints.py +338 -0
- praisonai/cli/features/code_intelligence.py +652 -0
- praisonai/cli/features/compaction.py +294 -0
- praisonai/cli/features/compare.py +534 -0
- praisonai/cli/features/cost_tracker.py +514 -0
- praisonai/cli/features/debug.py +810 -0
- praisonai/cli/features/deploy.py +517 -0
- praisonai/cli/features/diag.py +289 -0
- praisonai/cli/features/doctor/__init__.py +63 -0
- praisonai/cli/features/doctor/checks/__init__.py +24 -0
- praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
- praisonai/cli/features/doctor/checks/config_checks.py +366 -0
- praisonai/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai/cli/features/doctor/checks/env_checks.py +543 -0
- praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
- praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
- praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
- praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai/cli/features/doctor/engine.py +266 -0
- praisonai/cli/features/doctor/formatters.py +310 -0
- praisonai/cli/features/doctor/handler.py +397 -0
- praisonai/cli/features/doctor/models.py +264 -0
- praisonai/cli/features/doctor/registry.py +239 -0
- praisonai/cli/features/endpoints.py +1019 -0
- praisonai/cli/features/eval.py +560 -0
- praisonai/cli/features/external_agents.py +231 -0
- praisonai/cli/features/fast_context.py +410 -0
- praisonai/cli/features/flow_display.py +566 -0
- praisonai/cli/features/git_integration.py +651 -0
- praisonai/cli/features/guardrail.py +171 -0
- praisonai/cli/features/handoff.py +185 -0
- praisonai/cli/features/hooks.py +583 -0
- praisonai/cli/features/image.py +384 -0
- praisonai/cli/features/interactive_runtime.py +585 -0
- praisonai/cli/features/interactive_tools.py +380 -0
- praisonai/cli/features/interactive_tui.py +603 -0
- praisonai/cli/features/jobs.py +632 -0
- praisonai/cli/features/knowledge.py +531 -0
- praisonai/cli/features/lite.py +244 -0
- praisonai/cli/features/lsp_cli.py +225 -0
- praisonai/cli/features/mcp.py +169 -0
- praisonai/cli/features/message_queue.py +587 -0
- praisonai/cli/features/metrics.py +211 -0
- praisonai/cli/features/n8n.py +673 -0
- praisonai/cli/features/observability.py +293 -0
- praisonai/cli/features/ollama.py +361 -0
- praisonai/cli/features/output_style.py +273 -0
- praisonai/cli/features/package.py +631 -0
- praisonai/cli/features/performance.py +308 -0
- praisonai/cli/features/persistence.py +636 -0
- praisonai/cli/features/profile.py +226 -0
- praisonai/cli/features/profiler/__init__.py +81 -0
- praisonai/cli/features/profiler/core.py +558 -0
- praisonai/cli/features/profiler/optimizations.py +652 -0
- praisonai/cli/features/profiler/suite.py +386 -0
- praisonai/cli/features/profiling.py +350 -0
- praisonai/cli/features/queue/__init__.py +73 -0
- praisonai/cli/features/queue/manager.py +395 -0
- praisonai/cli/features/queue/models.py +286 -0
- praisonai/cli/features/queue/persistence.py +564 -0
- praisonai/cli/features/queue/scheduler.py +484 -0
- praisonai/cli/features/queue/worker.py +372 -0
- praisonai/cli/features/recipe.py +1723 -0
- praisonai/cli/features/recipes.py +449 -0
- praisonai/cli/features/registry.py +229 -0
- praisonai/cli/features/repo_map.py +860 -0
- praisonai/cli/features/router.py +466 -0
- praisonai/cli/features/sandbox_executor.py +515 -0
- praisonai/cli/features/serve.py +829 -0
- praisonai/cli/features/session.py +222 -0
- praisonai/cli/features/skills.py +856 -0
- praisonai/cli/features/slash_commands.py +650 -0
- praisonai/cli/features/telemetry.py +179 -0
- praisonai/cli/features/templates.py +1384 -0
- praisonai/cli/features/thinking.py +305 -0
- praisonai/cli/features/todo.py +334 -0
- praisonai/cli/features/tools.py +680 -0
- praisonai/cli/features/tui/__init__.py +83 -0
- praisonai/cli/features/tui/app.py +580 -0
- praisonai/cli/features/tui/cli.py +566 -0
- praisonai/cli/features/tui/debug.py +511 -0
- praisonai/cli/features/tui/events.py +99 -0
- praisonai/cli/features/tui/mock_provider.py +328 -0
- praisonai/cli/features/tui/orchestrator.py +652 -0
- praisonai/cli/features/tui/screens/__init__.py +50 -0
- praisonai/cli/features/tui/screens/main.py +245 -0
- praisonai/cli/features/tui/screens/queue.py +174 -0
- praisonai/cli/features/tui/screens/session.py +124 -0
- praisonai/cli/features/tui/screens/settings.py +148 -0
- praisonai/cli/features/tui/widgets/__init__.py +56 -0
- praisonai/cli/features/tui/widgets/chat.py +261 -0
- praisonai/cli/features/tui/widgets/composer.py +224 -0
- praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
- praisonai/cli/features/tui/widgets/status.py +167 -0
- praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
- praisonai/cli/features/workflow.py +720 -0
- praisonai/cli/legacy.py +236 -0
- praisonai/cli/main.py +5559 -0
- praisonai/cli/schedule_cli.py +54 -0
- praisonai/cli/state/__init__.py +31 -0
- praisonai/cli/state/identifiers.py +161 -0
- praisonai/cli/state/sessions.py +313 -0
- praisonai/code/__init__.py +93 -0
- praisonai/code/agent_tools.py +344 -0
- praisonai/code/diff/__init__.py +21 -0
- praisonai/code/diff/diff_strategy.py +432 -0
- praisonai/code/tools/__init__.py +27 -0
- praisonai/code/tools/apply_diff.py +221 -0
- praisonai/code/tools/execute_command.py +275 -0
- praisonai/code/tools/list_files.py +274 -0
- praisonai/code/tools/read_file.py +206 -0
- praisonai/code/tools/search_replace.py +248 -0
- praisonai/code/tools/write_file.py +217 -0
- praisonai/code/utils/__init__.py +46 -0
- praisonai/code/utils/file_utils.py +307 -0
- praisonai/code/utils/ignore_utils.py +308 -0
- praisonai/code/utils/text_utils.py +276 -0
- praisonai/db/__init__.py +64 -0
- praisonai/db/adapter.py +531 -0
- praisonai/deploy/__init__.py +62 -0
- praisonai/deploy/api.py +231 -0
- praisonai/deploy/docker.py +454 -0
- praisonai/deploy/doctor.py +367 -0
- praisonai/deploy/main.py +327 -0
- praisonai/deploy/models.py +179 -0
- praisonai/deploy/providers/__init__.py +33 -0
- praisonai/deploy/providers/aws.py +331 -0
- praisonai/deploy/providers/azure.py +358 -0
- praisonai/deploy/providers/base.py +101 -0
- praisonai/deploy/providers/gcp.py +314 -0
- praisonai/deploy/schema.py +208 -0
- praisonai/deploy.py +185 -0
- praisonai/endpoints/__init__.py +53 -0
- praisonai/endpoints/a2u_server.py +410 -0
- praisonai/endpoints/discovery.py +165 -0
- praisonai/endpoints/providers/__init__.py +28 -0
- praisonai/endpoints/providers/a2a.py +253 -0
- praisonai/endpoints/providers/a2u.py +208 -0
- praisonai/endpoints/providers/agents_api.py +171 -0
- praisonai/endpoints/providers/base.py +231 -0
- praisonai/endpoints/providers/mcp.py +263 -0
- praisonai/endpoints/providers/recipe.py +206 -0
- praisonai/endpoints/providers/tools_mcp.py +150 -0
- praisonai/endpoints/registry.py +131 -0
- praisonai/endpoints/server.py +161 -0
- praisonai/inbuilt_tools/__init__.py +24 -0
- praisonai/inbuilt_tools/autogen_tools.py +117 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/config.py +96 -0
- praisonai/inc/models.py +155 -0
- praisonai/integrations/__init__.py +56 -0
- praisonai/integrations/base.py +303 -0
- praisonai/integrations/claude_code.py +270 -0
- praisonai/integrations/codex_cli.py +255 -0
- praisonai/integrations/cursor_cli.py +195 -0
- praisonai/integrations/gemini_cli.py +222 -0
- praisonai/jobs/__init__.py +67 -0
- praisonai/jobs/executor.py +425 -0
- praisonai/jobs/models.py +230 -0
- praisonai/jobs/router.py +314 -0
- praisonai/jobs/server.py +186 -0
- praisonai/jobs/store.py +203 -0
- praisonai/llm/__init__.py +66 -0
- praisonai/llm/registry.py +382 -0
- praisonai/mcp_server/__init__.py +152 -0
- praisonai/mcp_server/adapters/__init__.py +74 -0
- praisonai/mcp_server/adapters/agents.py +128 -0
- praisonai/mcp_server/adapters/capabilities.py +168 -0
- praisonai/mcp_server/adapters/cli_tools.py +568 -0
- praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
- praisonai/mcp_server/adapters/knowledge.py +93 -0
- praisonai/mcp_server/adapters/memory.py +104 -0
- praisonai/mcp_server/adapters/prompts.py +306 -0
- praisonai/mcp_server/adapters/resources.py +124 -0
- praisonai/mcp_server/adapters/tools_bridge.py +280 -0
- praisonai/mcp_server/auth/__init__.py +48 -0
- praisonai/mcp_server/auth/api_key.py +291 -0
- praisonai/mcp_server/auth/oauth.py +460 -0
- praisonai/mcp_server/auth/oidc.py +289 -0
- praisonai/mcp_server/auth/scopes.py +260 -0
- praisonai/mcp_server/cli.py +852 -0
- praisonai/mcp_server/elicitation.py +445 -0
- praisonai/mcp_server/icons.py +302 -0
- praisonai/mcp_server/recipe_adapter.py +573 -0
- praisonai/mcp_server/recipe_cli.py +824 -0
- praisonai/mcp_server/registry.py +703 -0
- praisonai/mcp_server/sampling.py +422 -0
- praisonai/mcp_server/server.py +490 -0
- praisonai/mcp_server/tasks.py +443 -0
- praisonai/mcp_server/transports/__init__.py +18 -0
- praisonai/mcp_server/transports/http_stream.py +376 -0
- praisonai/mcp_server/transports/stdio.py +132 -0
- praisonai/persistence/__init__.py +84 -0
- praisonai/persistence/config.py +238 -0
- praisonai/persistence/conversation/__init__.py +25 -0
- praisonai/persistence/conversation/async_mysql.py +427 -0
- praisonai/persistence/conversation/async_postgres.py +410 -0
- praisonai/persistence/conversation/async_sqlite.py +371 -0
- praisonai/persistence/conversation/base.py +151 -0
- praisonai/persistence/conversation/json_store.py +250 -0
- praisonai/persistence/conversation/mysql.py +387 -0
- praisonai/persistence/conversation/postgres.py +401 -0
- praisonai/persistence/conversation/singlestore.py +240 -0
- praisonai/persistence/conversation/sqlite.py +341 -0
- praisonai/persistence/conversation/supabase.py +203 -0
- praisonai/persistence/conversation/surrealdb.py +287 -0
- praisonai/persistence/factory.py +301 -0
- praisonai/persistence/hooks/__init__.py +18 -0
- praisonai/persistence/hooks/agent_hooks.py +297 -0
- praisonai/persistence/knowledge/__init__.py +26 -0
- praisonai/persistence/knowledge/base.py +144 -0
- praisonai/persistence/knowledge/cassandra.py +232 -0
- praisonai/persistence/knowledge/chroma.py +295 -0
- praisonai/persistence/knowledge/clickhouse.py +242 -0
- praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
- praisonai/persistence/knowledge/couchbase.py +286 -0
- praisonai/persistence/knowledge/lancedb.py +216 -0
- praisonai/persistence/knowledge/langchain_adapter.py +291 -0
- praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
- praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
- praisonai/persistence/knowledge/milvus.py +277 -0
- praisonai/persistence/knowledge/mongodb_vector.py +306 -0
- praisonai/persistence/knowledge/pgvector.py +335 -0
- praisonai/persistence/knowledge/pinecone.py +253 -0
- praisonai/persistence/knowledge/qdrant.py +301 -0
- praisonai/persistence/knowledge/redis_vector.py +291 -0
- praisonai/persistence/knowledge/singlestore_vector.py +299 -0
- praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
- praisonai/persistence/knowledge/upstash_vector.py +266 -0
- praisonai/persistence/knowledge/weaviate.py +223 -0
- praisonai/persistence/migrations/__init__.py +10 -0
- praisonai/persistence/migrations/manager.py +251 -0
- praisonai/persistence/orchestrator.py +406 -0
- praisonai/persistence/state/__init__.py +21 -0
- praisonai/persistence/state/async_mongodb.py +200 -0
- praisonai/persistence/state/base.py +107 -0
- praisonai/persistence/state/dynamodb.py +226 -0
- praisonai/persistence/state/firestore.py +175 -0
- praisonai/persistence/state/gcs.py +155 -0
- praisonai/persistence/state/memory.py +245 -0
- praisonai/persistence/state/mongodb.py +158 -0
- praisonai/persistence/state/redis.py +190 -0
- praisonai/persistence/state/upstash.py +144 -0
- praisonai/persistence/tests/__init__.py +3 -0
- praisonai/persistence/tests/test_all_backends.py +633 -0
- praisonai/profiler.py +1214 -0
- praisonai/recipe/__init__.py +134 -0
- praisonai/recipe/bridge.py +278 -0
- praisonai/recipe/core.py +893 -0
- praisonai/recipe/exceptions.py +54 -0
- praisonai/recipe/history.py +402 -0
- praisonai/recipe/models.py +266 -0
- praisonai/recipe/operations.py +440 -0
- praisonai/recipe/policy.py +422 -0
- praisonai/recipe/registry.py +849 -0
- praisonai/recipe/runtime.py +214 -0
- praisonai/recipe/security.py +711 -0
- praisonai/recipe/serve.py +859 -0
- praisonai/recipe/server.py +613 -0
- praisonai/scheduler/__init__.py +45 -0
- praisonai/scheduler/agent_scheduler.py +552 -0
- praisonai/scheduler/base.py +124 -0
- praisonai/scheduler/daemon_manager.py +225 -0
- praisonai/scheduler/state_manager.py +155 -0
- praisonai/scheduler/yaml_loader.py +193 -0
- praisonai/scheduler.py +194 -0
- praisonai/setup/__init__.py +1 -0
- praisonai/setup/build.py +21 -0
- praisonai/setup/post_install.py +23 -0
- praisonai/setup/setup_conda_env.py +25 -0
- praisonai/setup.py +16 -0
- praisonai/templates/__init__.py +116 -0
- praisonai/templates/cache.py +364 -0
- praisonai/templates/dependency_checker.py +358 -0
- praisonai/templates/discovery.py +391 -0
- praisonai/templates/loader.py +564 -0
- praisonai/templates/registry.py +511 -0
- praisonai/templates/resolver.py +206 -0
- praisonai/templates/security.py +327 -0
- praisonai/templates/tool_override.py +498 -0
- praisonai/templates/tools_doctor.py +256 -0
- praisonai/test.py +105 -0
- praisonai/train.py +562 -0
- praisonai/train_vision.py +306 -0
- praisonai/ui/agents.py +824 -0
- praisonai/ui/callbacks.py +57 -0
- praisonai/ui/chainlit_compat.py +246 -0
- praisonai/ui/chat.py +532 -0
- praisonai/ui/code.py +717 -0
- praisonai/ui/colab.py +474 -0
- praisonai/ui/colab_chainlit.py +81 -0
- praisonai/ui/components/aicoder.py +284 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/database_config.py +56 -0
- praisonai/ui/db.py +294 -0
- praisonai/ui/realtime.py +488 -0
- praisonai/ui/realtimeclient/__init__.py +756 -0
- praisonai/ui/realtimeclient/tools.py +242 -0
- praisonai/ui/sql_alchemy.py +710 -0
- praisonai/upload_vision.py +140 -0
- praisonai/version.py +1 -0
- praisonai-3.0.0.dist-info/METADATA +3493 -0
- praisonai-3.0.0.dist-info/RECORD +393 -0
- praisonai-3.0.0.dist-info/WHEEL +5 -0
- praisonai-3.0.0.dist-info/entry_points.txt +4 -0
- praisonai-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-Search-Replace Diff Strategy for PraisonAI Code.
|
|
3
|
+
|
|
4
|
+
This module implements a diff strategy that applies SEARCH/REPLACE blocks
|
|
5
|
+
to file content with fuzzy matching support, similar to Kilo Code's approach.
|
|
6
|
+
|
|
7
|
+
Diff Format:
|
|
8
|
+
<<<<<<< SEARCH
|
|
9
|
+
:start_line:N
|
|
10
|
+
-------
|
|
11
|
+
[exact content to find]
|
|
12
|
+
=======
|
|
13
|
+
[new content to replace with]
|
|
14
|
+
>>>>>>> REPLACE
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from ..utils.text_utils import (
|
|
22
|
+
get_similarity,
|
|
23
|
+
fuzzy_search,
|
|
24
|
+
get_indentation,
|
|
25
|
+
)
|
|
26
|
+
from ..utils.file_utils import (
|
|
27
|
+
strip_line_numbers,
|
|
28
|
+
every_line_has_line_numbers,
|
|
29
|
+
add_line_numbers,
|
|
30
|
+
detect_line_ending,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Default buffer lines for fuzzy search around start_line hint
|
|
35
|
+
BUFFER_LINES = 40
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DiffBlock:
|
|
40
|
+
"""
|
|
41
|
+
Represents a single SEARCH/REPLACE block.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
search_content: The content to search for
|
|
45
|
+
replace_content: The content to replace with
|
|
46
|
+
start_line: Optional line number hint (1-indexed)
|
|
47
|
+
end_line: Optional end line hint (1-indexed)
|
|
48
|
+
"""
|
|
49
|
+
search_content: str
|
|
50
|
+
replace_content: str
|
|
51
|
+
start_line: Optional[int] = None
|
|
52
|
+
end_line: Optional[int] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class DiffResult:
|
|
57
|
+
"""
|
|
58
|
+
Result of applying a diff operation.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
success: Whether the diff was applied successfully
|
|
62
|
+
content: The resulting content (if successful)
|
|
63
|
+
error: Error message (if failed)
|
|
64
|
+
applied_count: Number of blocks successfully applied
|
|
65
|
+
failed_blocks: List of failed block results
|
|
66
|
+
"""
|
|
67
|
+
success: bool
|
|
68
|
+
content: Optional[str] = None
|
|
69
|
+
error: Optional[str] = None
|
|
70
|
+
applied_count: int = 0
|
|
71
|
+
failed_blocks: List[dict] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _unescape_markers(content: str) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Unescape escaped diff markers in content.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
content: Content with potentially escaped markers
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Content with markers unescaped
|
|
83
|
+
"""
|
|
84
|
+
replacements = [
|
|
85
|
+
(r'^\\\<\<\<\<\<\<\<', '<<<<<<<'),
|
|
86
|
+
(r'^\\=======', '======='),
|
|
87
|
+
(r'^\\\>\>\>\>\>\>\>', '>>>>>>>'),
|
|
88
|
+
(r'^\\-------', '-------'),
|
|
89
|
+
(r'^\\:end_line:', ':end_line:'),
|
|
90
|
+
(r'^\\:start_line:', ':start_line:'),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
result = content
|
|
94
|
+
for pattern, replacement in replacements:
|
|
95
|
+
result = re.sub(pattern, replacement, result, flags=re.MULTILINE)
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validate_diff_format(diff_content: str) -> Tuple[bool, Optional[str]]:
|
|
101
|
+
"""
|
|
102
|
+
Validate that a diff string has correct marker sequencing.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
diff_content: The diff content to validate
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Tuple of (is_valid, error_message)
|
|
109
|
+
"""
|
|
110
|
+
# State machine states
|
|
111
|
+
STATE_START = 0
|
|
112
|
+
STATE_AFTER_SEARCH = 1
|
|
113
|
+
STATE_AFTER_SEPARATOR = 2
|
|
114
|
+
|
|
115
|
+
state = STATE_START
|
|
116
|
+
line_num = 0
|
|
117
|
+
|
|
118
|
+
SEARCH_PATTERN = re.compile(r'^<<<<<<< SEARCH>?$')
|
|
119
|
+
SEP = '======='
|
|
120
|
+
REPLACE = '>>>>>>> REPLACE'
|
|
121
|
+
|
|
122
|
+
lines = diff_content.split('\n')
|
|
123
|
+
|
|
124
|
+
for line in lines:
|
|
125
|
+
line_num += 1
|
|
126
|
+
marker = line.strip()
|
|
127
|
+
|
|
128
|
+
# Check for line markers in REPLACE sections
|
|
129
|
+
if state == STATE_AFTER_SEPARATOR:
|
|
130
|
+
if marker.startswith(':start_line:') and not line.strip().startswith('\\:start_line:'):
|
|
131
|
+
return False, f"ERROR: Line marker ':start_line:' found in REPLACE section at line {line_num}"
|
|
132
|
+
if marker.startswith(':end_line:') and not line.strip().startswith('\\:end_line:'):
|
|
133
|
+
return False, f"ERROR: Line marker ':end_line:' found in REPLACE section at line {line_num}"
|
|
134
|
+
|
|
135
|
+
if state == STATE_START:
|
|
136
|
+
if marker == SEP:
|
|
137
|
+
return False, f"ERROR: Unexpected '=======' at line {line_num}, expected '<<<<<<< SEARCH'"
|
|
138
|
+
if marker == REPLACE:
|
|
139
|
+
return False, f"ERROR: Unexpected '>>>>>>> REPLACE' at line {line_num}"
|
|
140
|
+
if SEARCH_PATTERN.match(marker):
|
|
141
|
+
state = STATE_AFTER_SEARCH
|
|
142
|
+
|
|
143
|
+
elif state == STATE_AFTER_SEARCH:
|
|
144
|
+
if SEARCH_PATTERN.match(marker):
|
|
145
|
+
return False, f"ERROR: Duplicate '<<<<<<< SEARCH' at line {line_num}"
|
|
146
|
+
if marker == REPLACE:
|
|
147
|
+
return False, f"ERROR: Missing '=======' before '>>>>>>> REPLACE' at line {line_num}"
|
|
148
|
+
if marker == SEP:
|
|
149
|
+
state = STATE_AFTER_SEPARATOR
|
|
150
|
+
|
|
151
|
+
elif state == STATE_AFTER_SEPARATOR:
|
|
152
|
+
if SEARCH_PATTERN.match(marker):
|
|
153
|
+
return False, f"ERROR: Unexpected '<<<<<<< SEARCH' at line {line_num}, expected '>>>>>>> REPLACE'"
|
|
154
|
+
if marker == SEP:
|
|
155
|
+
return False, f"ERROR: Duplicate '=======' at line {line_num}"
|
|
156
|
+
if marker == REPLACE:
|
|
157
|
+
state = STATE_START
|
|
158
|
+
|
|
159
|
+
if state != STATE_START:
|
|
160
|
+
expected = "'======='" if state == STATE_AFTER_SEARCH else "'>>>>>>> REPLACE'"
|
|
161
|
+
return False, f"ERROR: Unexpected end of diff, expected {expected}"
|
|
162
|
+
|
|
163
|
+
return True, None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def parse_diff_blocks(diff_content: str) -> Tuple[List[DiffBlock], Optional[str]]:
|
|
167
|
+
"""
|
|
168
|
+
Parse SEARCH/REPLACE blocks from diff content.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
diff_content: The diff content to parse
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Tuple of (list of DiffBlock, error message if any)
|
|
175
|
+
"""
|
|
176
|
+
# Validate format first
|
|
177
|
+
is_valid, error = validate_diff_format(diff_content)
|
|
178
|
+
if not is_valid:
|
|
179
|
+
return [], error
|
|
180
|
+
|
|
181
|
+
# Regex to match SEARCH/REPLACE blocks
|
|
182
|
+
# Groups: 1=start_line line, 2=start_line number, 3=end_line line, 4=end_line number,
|
|
183
|
+
# 5=separator line, 6=search content, 7=replace content
|
|
184
|
+
pattern = re.compile(
|
|
185
|
+
r'(?:^|\n)(?<!\\)<<<<<<< SEARCH>?\s*\n'
|
|
186
|
+
r'((?::start_line:\s*(\d+)\s*\n))?'
|
|
187
|
+
r'((?::end_line:\s*(\d+)\s*\n))?'
|
|
188
|
+
r'((?<!\\)-------\s*\n)?'
|
|
189
|
+
r'([\s\S]*?)(?:\n)?'
|
|
190
|
+
r'(?:(?<=\n)(?<!\\)=======\s*\n)'
|
|
191
|
+
r'([\s\S]*?)(?:\n)?'
|
|
192
|
+
r'(?:(?<=\n)(?<!\\)>>>>>>> REPLACE)(?=\n|$)',
|
|
193
|
+
re.MULTILINE
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
matches = list(pattern.finditer(diff_content))
|
|
197
|
+
|
|
198
|
+
if not matches:
|
|
199
|
+
return [], "Invalid diff format - no valid SEARCH/REPLACE blocks found"
|
|
200
|
+
|
|
201
|
+
blocks = []
|
|
202
|
+
for match in matches:
|
|
203
|
+
start_line = int(match.group(2)) if match.group(2) else None
|
|
204
|
+
end_line = int(match.group(4)) if match.group(4) else None
|
|
205
|
+
search_content = match.group(6) or ""
|
|
206
|
+
replace_content = match.group(7) or ""
|
|
207
|
+
|
|
208
|
+
blocks.append(DiffBlock(
|
|
209
|
+
search_content=search_content,
|
|
210
|
+
replace_content=replace_content,
|
|
211
|
+
start_line=start_line,
|
|
212
|
+
end_line=end_line,
|
|
213
|
+
))
|
|
214
|
+
|
|
215
|
+
# Sort by start_line (blocks without start_line go last)
|
|
216
|
+
blocks.sort(key=lambda b: b.start_line if b.start_line else float('inf'))
|
|
217
|
+
|
|
218
|
+
return blocks, None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def apply_search_replace_diff(
|
|
222
|
+
original_content: str,
|
|
223
|
+
diff_content: str,
|
|
224
|
+
fuzzy_threshold: float = 1.0,
|
|
225
|
+
buffer_lines: int = BUFFER_LINES,
|
|
226
|
+
) -> DiffResult:
|
|
227
|
+
"""
|
|
228
|
+
Apply SEARCH/REPLACE diff blocks to original content.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
original_content: The original file content
|
|
232
|
+
diff_content: The diff content with SEARCH/REPLACE blocks
|
|
233
|
+
fuzzy_threshold: Similarity threshold (0.0-1.0, 1.0 = exact match)
|
|
234
|
+
buffer_lines: Number of lines to search around start_line hint
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
DiffResult with success status and resulting content
|
|
238
|
+
"""
|
|
239
|
+
# Parse diff blocks
|
|
240
|
+
blocks, parse_error = parse_diff_blocks(diff_content)
|
|
241
|
+
if parse_error:
|
|
242
|
+
return DiffResult(success=False, error=parse_error)
|
|
243
|
+
|
|
244
|
+
if not blocks:
|
|
245
|
+
return DiffResult(
|
|
246
|
+
success=False,
|
|
247
|
+
error="No valid SEARCH/REPLACE blocks found in diff"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Detect line ending from original content
|
|
251
|
+
line_ending = detect_line_ending(original_content)
|
|
252
|
+
result_lines = original_content.split('\n')
|
|
253
|
+
if original_content.endswith('\n'):
|
|
254
|
+
# Handle trailing newline
|
|
255
|
+
if result_lines and result_lines[-1] == '':
|
|
256
|
+
result_lines = result_lines[:-1]
|
|
257
|
+
|
|
258
|
+
delta = 0 # Track line number changes from previous replacements
|
|
259
|
+
applied_count = 0
|
|
260
|
+
failed_blocks = []
|
|
261
|
+
|
|
262
|
+
for block in blocks:
|
|
263
|
+
search_content = _unescape_markers(block.search_content)
|
|
264
|
+
replace_content = _unescape_markers(block.replace_content)
|
|
265
|
+
|
|
266
|
+
# Handle line numbers in content
|
|
267
|
+
has_all_line_numbers = (
|
|
268
|
+
(every_line_has_line_numbers(search_content) and every_line_has_line_numbers(replace_content)) or
|
|
269
|
+
(every_line_has_line_numbers(search_content) and not replace_content.strip())
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
start_line = block.start_line
|
|
273
|
+
if has_all_line_numbers and not start_line:
|
|
274
|
+
# Extract start line from first line number in search content
|
|
275
|
+
first_line = search_content.split('\n')[0]
|
|
276
|
+
match = re.match(r'\s*(\d+)', first_line)
|
|
277
|
+
if match:
|
|
278
|
+
start_line = int(match.group(1))
|
|
279
|
+
|
|
280
|
+
if has_all_line_numbers:
|
|
281
|
+
search_content = strip_line_numbers(search_content)
|
|
282
|
+
replace_content = strip_line_numbers(replace_content)
|
|
283
|
+
|
|
284
|
+
# Validate search content
|
|
285
|
+
if search_content == replace_content:
|
|
286
|
+
failed_blocks.append({
|
|
287
|
+
'error': 'Search and replace content are identical - no changes would be made',
|
|
288
|
+
'search': search_content[:100],
|
|
289
|
+
})
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
search_lines = search_content.split('\n') if search_content else []
|
|
293
|
+
replace_lines = replace_content.split('\n') if replace_content else []
|
|
294
|
+
|
|
295
|
+
if not search_lines or (len(search_lines) == 1 and not search_lines[0]):
|
|
296
|
+
failed_blocks.append({
|
|
297
|
+
'error': 'Empty search content is not allowed',
|
|
298
|
+
})
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
# Apply delta to start_line
|
|
302
|
+
adjusted_start = start_line + delta if start_line else None
|
|
303
|
+
|
|
304
|
+
# Initialize search
|
|
305
|
+
match_index = -1
|
|
306
|
+
best_score = 0.0
|
|
307
|
+
best_match_content = ""
|
|
308
|
+
search_chunk = '\n'.join(search_lines)
|
|
309
|
+
|
|
310
|
+
# Determine search bounds
|
|
311
|
+
search_start_index = 0
|
|
312
|
+
search_end_index = len(result_lines)
|
|
313
|
+
|
|
314
|
+
if adjusted_start:
|
|
315
|
+
# Try exact match at start_line first
|
|
316
|
+
exact_start_index = adjusted_start - 1 # Convert to 0-indexed
|
|
317
|
+
exact_end_index = exact_start_index + len(search_lines)
|
|
318
|
+
|
|
319
|
+
if 0 <= exact_start_index < len(result_lines):
|
|
320
|
+
original_chunk = '\n'.join(result_lines[exact_start_index:exact_end_index])
|
|
321
|
+
similarity = get_similarity(original_chunk, search_chunk)
|
|
322
|
+
|
|
323
|
+
if similarity >= fuzzy_threshold:
|
|
324
|
+
match_index = exact_start_index
|
|
325
|
+
best_score = similarity
|
|
326
|
+
best_match_content = original_chunk
|
|
327
|
+
else:
|
|
328
|
+
# Set bounds for buffered search
|
|
329
|
+
search_start_index = max(0, adjusted_start - buffer_lines - 1)
|
|
330
|
+
search_end_index = min(len(result_lines), adjusted_start + len(search_lines) + buffer_lines)
|
|
331
|
+
|
|
332
|
+
# If no match found yet, try fuzzy search
|
|
333
|
+
if match_index == -1:
|
|
334
|
+
score, idx, content = fuzzy_search(
|
|
335
|
+
result_lines, search_chunk, search_start_index, search_end_index
|
|
336
|
+
)
|
|
337
|
+
match_index = idx
|
|
338
|
+
best_score = score
|
|
339
|
+
best_match_content = content
|
|
340
|
+
|
|
341
|
+
# Check if match meets threshold
|
|
342
|
+
if match_index == -1 or best_score < fuzzy_threshold:
|
|
343
|
+
# Try aggressive line number stripping as fallback
|
|
344
|
+
aggressive_search = strip_line_numbers(search_content, aggressive=True)
|
|
345
|
+
aggressive_replace = strip_line_numbers(replace_content, aggressive=True)
|
|
346
|
+
|
|
347
|
+
if aggressive_search != search_content:
|
|
348
|
+
aggressive_lines = aggressive_search.split('\n') if aggressive_search else []
|
|
349
|
+
aggressive_chunk = '\n'.join(aggressive_lines)
|
|
350
|
+
|
|
351
|
+
score, idx, content = fuzzy_search(
|
|
352
|
+
result_lines, aggressive_chunk, search_start_index, search_end_index
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if idx != -1 and score >= fuzzy_threshold:
|
|
356
|
+
match_index = idx
|
|
357
|
+
best_score = score
|
|
358
|
+
best_match_content = content
|
|
359
|
+
search_content = aggressive_search
|
|
360
|
+
replace_content = aggressive_replace
|
|
361
|
+
search_lines = aggressive_lines
|
|
362
|
+
replace_lines = replace_content.split('\n') if replace_content else []
|
|
363
|
+
|
|
364
|
+
# Still no match - report error
|
|
365
|
+
if match_index == -1 or best_score < fuzzy_threshold:
|
|
366
|
+
line_info = f" at line {start_line}" if start_line else ""
|
|
367
|
+
|
|
368
|
+
# Build context for error message
|
|
369
|
+
context_start = max(0, (adjusted_start or 1) - 1 - buffer_lines)
|
|
370
|
+
context_end = min(len(result_lines), (adjusted_start or 1) + len(search_lines) + buffer_lines)
|
|
371
|
+
original_context = '\n'.join(result_lines[context_start:context_end])
|
|
372
|
+
|
|
373
|
+
failed_blocks.append({
|
|
374
|
+
'error': f"No sufficiently similar match found{line_info} ({int(best_score * 100)}% similar, needs {int(fuzzy_threshold * 100)}%)",
|
|
375
|
+
'search_content': search_chunk[:200],
|
|
376
|
+
'best_match': best_match_content[:200] if best_match_content else None,
|
|
377
|
+
'similarity': best_score,
|
|
378
|
+
'context': add_line_numbers(original_context, context_start + 1)[:500],
|
|
379
|
+
})
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
# Apply the replacement with indentation preservation
|
|
383
|
+
matched_lines = result_lines[match_index:match_index + len(search_lines)]
|
|
384
|
+
|
|
385
|
+
# Get indentation from original
|
|
386
|
+
original_indents = [get_indentation(line) for line in matched_lines]
|
|
387
|
+
search_indents = [get_indentation(line) for line in search_lines]
|
|
388
|
+
|
|
389
|
+
# Apply replacement with preserved indentation
|
|
390
|
+
indented_replace_lines = []
|
|
391
|
+
for i, line in enumerate(replace_lines):
|
|
392
|
+
matched_indent = original_indents[0] if original_indents else ""
|
|
393
|
+
current_indent = get_indentation(line)
|
|
394
|
+
search_base_indent = search_indents[0] if search_indents else ""
|
|
395
|
+
|
|
396
|
+
# Calculate relative indentation
|
|
397
|
+
search_base_level = len(search_base_indent)
|
|
398
|
+
current_level = len(current_indent)
|
|
399
|
+
relative_level = current_level - search_base_level
|
|
400
|
+
|
|
401
|
+
if relative_level < 0:
|
|
402
|
+
final_indent = matched_indent[:max(0, len(matched_indent) + relative_level)]
|
|
403
|
+
else:
|
|
404
|
+
final_indent = matched_indent + current_indent[search_base_level:]
|
|
405
|
+
|
|
406
|
+
indented_replace_lines.append(final_indent + line.lstrip())
|
|
407
|
+
|
|
408
|
+
# Construct final content
|
|
409
|
+
before_match = result_lines[:match_index]
|
|
410
|
+
after_match = result_lines[match_index + len(search_lines):]
|
|
411
|
+
result_lines = before_match + indented_replace_lines + after_match
|
|
412
|
+
|
|
413
|
+
# Update delta
|
|
414
|
+
delta += len(replace_lines) - len(matched_lines)
|
|
415
|
+
applied_count += 1
|
|
416
|
+
|
|
417
|
+
# Build final content
|
|
418
|
+
final_content = line_ending.join(result_lines)
|
|
419
|
+
|
|
420
|
+
if applied_count == 0:
|
|
421
|
+
return DiffResult(
|
|
422
|
+
success=False,
|
|
423
|
+
error="No blocks were successfully applied",
|
|
424
|
+
failed_blocks=failed_blocks,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return DiffResult(
|
|
428
|
+
success=True,
|
|
429
|
+
content=final_content,
|
|
430
|
+
applied_count=applied_count,
|
|
431
|
+
failed_blocks=failed_blocks,
|
|
432
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code editing tools for PraisonAI agents.
|
|
3
|
+
|
|
4
|
+
Provides tools for:
|
|
5
|
+
- read_file: Read file contents with optional line ranges
|
|
6
|
+
- write_file: Create or overwrite files
|
|
7
|
+
- list_files: List directory contents
|
|
8
|
+
- apply_diff: Apply SEARCH/REPLACE diffs
|
|
9
|
+
- search_replace: Multiple search/replace operations
|
|
10
|
+
- execute_command: Run shell commands
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .read_file import read_file
|
|
14
|
+
from .write_file import write_file
|
|
15
|
+
from .list_files import list_files
|
|
16
|
+
from .apply_diff import apply_diff
|
|
17
|
+
from .search_replace import search_replace
|
|
18
|
+
from .execute_command import execute_command
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"read_file",
|
|
22
|
+
"write_file",
|
|
23
|
+
"list_files",
|
|
24
|
+
"apply_diff",
|
|
25
|
+
"search_replace",
|
|
26
|
+
"execute_command",
|
|
27
|
+
]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Apply Diff Tool for PraisonAI Code.
|
|
3
|
+
|
|
4
|
+
Provides functionality to apply SEARCH/REPLACE diffs to files
|
|
5
|
+
with fuzzy matching support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Optional, Dict, Any
|
|
10
|
+
|
|
11
|
+
from ..diff.diff_strategy import apply_search_replace_diff
|
|
12
|
+
from ..utils.file_utils import (
|
|
13
|
+
file_exists,
|
|
14
|
+
is_path_within_directory,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def apply_diff(
|
|
19
|
+
path: str,
|
|
20
|
+
diff: str,
|
|
21
|
+
workspace: Optional[str] = None,
|
|
22
|
+
fuzzy_threshold: float = 1.0,
|
|
23
|
+
buffer_lines: int = 40,
|
|
24
|
+
backup: bool = False,
|
|
25
|
+
encoding: str = 'utf-8',
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Apply a SEARCH/REPLACE diff to a file.
|
|
29
|
+
|
|
30
|
+
This tool applies precise, targeted modifications to an existing file
|
|
31
|
+
by searching for specific sections of content and replacing them.
|
|
32
|
+
|
|
33
|
+
Diff Format:
|
|
34
|
+
<<<<<<< SEARCH
|
|
35
|
+
:start_line:N
|
|
36
|
+
-------
|
|
37
|
+
[exact content to find]
|
|
38
|
+
=======
|
|
39
|
+
[new content to replace with]
|
|
40
|
+
>>>>>>> REPLACE
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
path: Path to the file (absolute or relative to workspace)
|
|
44
|
+
diff: The SEARCH/REPLACE diff content
|
|
45
|
+
workspace: Workspace root directory (for relative paths)
|
|
46
|
+
fuzzy_threshold: Similarity threshold (0.0-1.0, 1.0 = exact match)
|
|
47
|
+
buffer_lines: Lines to search around start_line hint
|
|
48
|
+
backup: Whether to create a backup before modifying
|
|
49
|
+
encoding: File encoding (default: utf-8)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dictionary with:
|
|
53
|
+
- success: bool
|
|
54
|
+
- path: str
|
|
55
|
+
- applied_count: int (number of blocks applied)
|
|
56
|
+
- failed_blocks: list (details of failed blocks)
|
|
57
|
+
- error: str (if success is False)
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> diff = '''
|
|
61
|
+
... <<<<<<< SEARCH
|
|
62
|
+
... :start_line:1
|
|
63
|
+
... -------
|
|
64
|
+
... def old_function():
|
|
65
|
+
... pass
|
|
66
|
+
... =======
|
|
67
|
+
... def new_function():
|
|
68
|
+
... return True
|
|
69
|
+
... >>>>>>> REPLACE
|
|
70
|
+
... '''
|
|
71
|
+
>>> result = apply_diff("src/main.py", diff)
|
|
72
|
+
"""
|
|
73
|
+
# Resolve path
|
|
74
|
+
if workspace and not os.path.isabs(path):
|
|
75
|
+
abs_path = os.path.abspath(os.path.join(workspace, path))
|
|
76
|
+
else:
|
|
77
|
+
abs_path = os.path.abspath(path)
|
|
78
|
+
|
|
79
|
+
# Security check
|
|
80
|
+
if workspace:
|
|
81
|
+
if not is_path_within_directory(abs_path, workspace):
|
|
82
|
+
return {
|
|
83
|
+
'success': False,
|
|
84
|
+
'error': f"Path '{path}' is outside the workspace",
|
|
85
|
+
'path': path,
|
|
86
|
+
'applied_count': 0,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Check if file exists
|
|
90
|
+
if not file_exists(abs_path):
|
|
91
|
+
return {
|
|
92
|
+
'success': False,
|
|
93
|
+
'error': f"File not found: {path}",
|
|
94
|
+
'path': path,
|
|
95
|
+
'applied_count': 0,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# Read original content
|
|
100
|
+
with open(abs_path, 'r', encoding=encoding, errors='replace') as f:
|
|
101
|
+
original_content = f.read()
|
|
102
|
+
|
|
103
|
+
# Apply the diff
|
|
104
|
+
result = apply_search_replace_diff(
|
|
105
|
+
original_content=original_content,
|
|
106
|
+
diff_content=diff,
|
|
107
|
+
fuzzy_threshold=fuzzy_threshold,
|
|
108
|
+
buffer_lines=buffer_lines,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not result.success:
|
|
112
|
+
return {
|
|
113
|
+
'success': False,
|
|
114
|
+
'error': result.error or "Failed to apply diff",
|
|
115
|
+
'path': path,
|
|
116
|
+
'applied_count': result.applied_count,
|
|
117
|
+
'failed_blocks': result.failed_blocks,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Create backup if requested
|
|
121
|
+
backup_path = None
|
|
122
|
+
if backup:
|
|
123
|
+
import time
|
|
124
|
+
timestamp = int(time.time())
|
|
125
|
+
backup_path = f"{abs_path}.backup.{timestamp}"
|
|
126
|
+
with open(backup_path, 'w', encoding=encoding) as f:
|
|
127
|
+
f.write(original_content)
|
|
128
|
+
|
|
129
|
+
# Write the modified content
|
|
130
|
+
with open(abs_path, 'w', encoding=encoding) as f:
|
|
131
|
+
f.write(result.content)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
'success': True,
|
|
135
|
+
'path': path,
|
|
136
|
+
'absolute_path': abs_path,
|
|
137
|
+
'applied_count': result.applied_count,
|
|
138
|
+
'failed_blocks': result.failed_blocks if result.failed_blocks else None,
|
|
139
|
+
'backup_path': backup_path,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
except PermissionError:
|
|
143
|
+
return {
|
|
144
|
+
'success': False,
|
|
145
|
+
'error': f"Permission denied: {path}",
|
|
146
|
+
'path': path,
|
|
147
|
+
'applied_count': 0,
|
|
148
|
+
}
|
|
149
|
+
except Exception as e:
|
|
150
|
+
return {
|
|
151
|
+
'success': False,
|
|
152
|
+
'error': f"Error applying diff to {path}: {str(e)}",
|
|
153
|
+
'path': path,
|
|
154
|
+
'applied_count': 0,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def create_diff_block(
|
|
159
|
+
search_content: str,
|
|
160
|
+
replace_content: str,
|
|
161
|
+
start_line: Optional[int] = None,
|
|
162
|
+
) -> str:
|
|
163
|
+
"""
|
|
164
|
+
Create a properly formatted SEARCH/REPLACE diff block.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
search_content: The content to search for
|
|
168
|
+
replace_content: The content to replace with
|
|
169
|
+
start_line: Optional line number hint
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Formatted diff block string
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> diff = create_diff_block(
|
|
176
|
+
... "def old():\\n pass",
|
|
177
|
+
... "def new():\\n return True",
|
|
178
|
+
... start_line=10
|
|
179
|
+
... )
|
|
180
|
+
"""
|
|
181
|
+
lines = ["<<<<<<< SEARCH"]
|
|
182
|
+
|
|
183
|
+
if start_line:
|
|
184
|
+
lines.append(f":start_line:{start_line}")
|
|
185
|
+
|
|
186
|
+
lines.append("-------")
|
|
187
|
+
lines.append(search_content)
|
|
188
|
+
lines.append("=======")
|
|
189
|
+
lines.append(replace_content)
|
|
190
|
+
lines.append(">>>>>>> REPLACE")
|
|
191
|
+
|
|
192
|
+
return '\n'.join(lines)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def create_multi_diff(blocks: list) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Create a diff with multiple SEARCH/REPLACE blocks.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
blocks: List of dicts with 'search', 'replace', and optional 'start_line'
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Combined diff string with all blocks
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
>>> diff = create_multi_diff([
|
|
207
|
+
... {'search': 'old1', 'replace': 'new1', 'start_line': 5},
|
|
208
|
+
... {'search': 'old2', 'replace': 'new2', 'start_line': 20},
|
|
209
|
+
... ])
|
|
210
|
+
"""
|
|
211
|
+
diff_blocks = []
|
|
212
|
+
|
|
213
|
+
for block in blocks:
|
|
214
|
+
diff_block = create_diff_block(
|
|
215
|
+
search_content=block.get('search', ''),
|
|
216
|
+
replace_content=block.get('replace', ''),
|
|
217
|
+
start_line=block.get('start_line'),
|
|
218
|
+
)
|
|
219
|
+
diff_blocks.append(diff_block)
|
|
220
|
+
|
|
221
|
+
return '\n\n'.join(diff_blocks)
|