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.
Files changed (393) hide show
  1. praisonai/__init__.py +54 -0
  2. praisonai/__main__.py +15 -0
  3. praisonai/acp/__init__.py +54 -0
  4. praisonai/acp/config.py +159 -0
  5. praisonai/acp/server.py +587 -0
  6. praisonai/acp/session.py +219 -0
  7. praisonai/adapters/__init__.py +50 -0
  8. praisonai/adapters/readers.py +395 -0
  9. praisonai/adapters/rerankers.py +315 -0
  10. praisonai/adapters/retrievers.py +394 -0
  11. praisonai/adapters/vector_stores.py +409 -0
  12. praisonai/agent_scheduler.py +337 -0
  13. praisonai/agents_generator.py +903 -0
  14. praisonai/api/call.py +292 -0
  15. praisonai/auto.py +1197 -0
  16. praisonai/capabilities/__init__.py +275 -0
  17. praisonai/capabilities/a2a.py +140 -0
  18. praisonai/capabilities/assistants.py +283 -0
  19. praisonai/capabilities/audio.py +320 -0
  20. praisonai/capabilities/batches.py +469 -0
  21. praisonai/capabilities/completions.py +336 -0
  22. praisonai/capabilities/container_files.py +155 -0
  23. praisonai/capabilities/containers.py +93 -0
  24. praisonai/capabilities/embeddings.py +158 -0
  25. praisonai/capabilities/files.py +467 -0
  26. praisonai/capabilities/fine_tuning.py +293 -0
  27. praisonai/capabilities/guardrails.py +182 -0
  28. praisonai/capabilities/images.py +330 -0
  29. praisonai/capabilities/mcp.py +190 -0
  30. praisonai/capabilities/messages.py +270 -0
  31. praisonai/capabilities/moderations.py +154 -0
  32. praisonai/capabilities/ocr.py +217 -0
  33. praisonai/capabilities/passthrough.py +204 -0
  34. praisonai/capabilities/rag.py +207 -0
  35. praisonai/capabilities/realtime.py +160 -0
  36. praisonai/capabilities/rerank.py +165 -0
  37. praisonai/capabilities/responses.py +266 -0
  38. praisonai/capabilities/search.py +109 -0
  39. praisonai/capabilities/skills.py +133 -0
  40. praisonai/capabilities/vector_store_files.py +334 -0
  41. praisonai/capabilities/vector_stores.py +304 -0
  42. praisonai/capabilities/videos.py +141 -0
  43. praisonai/chainlit_ui.py +304 -0
  44. praisonai/chat/__init__.py +106 -0
  45. praisonai/chat/app.py +125 -0
  46. praisonai/cli/__init__.py +26 -0
  47. praisonai/cli/app.py +213 -0
  48. praisonai/cli/commands/__init__.py +75 -0
  49. praisonai/cli/commands/acp.py +70 -0
  50. praisonai/cli/commands/completion.py +333 -0
  51. praisonai/cli/commands/config.py +166 -0
  52. praisonai/cli/commands/debug.py +142 -0
  53. praisonai/cli/commands/diag.py +55 -0
  54. praisonai/cli/commands/doctor.py +166 -0
  55. praisonai/cli/commands/environment.py +179 -0
  56. praisonai/cli/commands/lsp.py +112 -0
  57. praisonai/cli/commands/mcp.py +210 -0
  58. praisonai/cli/commands/profile.py +457 -0
  59. praisonai/cli/commands/run.py +228 -0
  60. praisonai/cli/commands/schedule.py +150 -0
  61. praisonai/cli/commands/serve.py +97 -0
  62. praisonai/cli/commands/session.py +212 -0
  63. praisonai/cli/commands/traces.py +145 -0
  64. praisonai/cli/commands/version.py +101 -0
  65. praisonai/cli/configuration/__init__.py +18 -0
  66. praisonai/cli/configuration/loader.py +353 -0
  67. praisonai/cli/configuration/paths.py +114 -0
  68. praisonai/cli/configuration/schema.py +164 -0
  69. praisonai/cli/features/__init__.py +268 -0
  70. praisonai/cli/features/acp.py +236 -0
  71. praisonai/cli/features/action_orchestrator.py +546 -0
  72. praisonai/cli/features/agent_scheduler.py +773 -0
  73. praisonai/cli/features/agent_tools.py +474 -0
  74. praisonai/cli/features/agents.py +375 -0
  75. praisonai/cli/features/at_mentions.py +471 -0
  76. praisonai/cli/features/auto_memory.py +182 -0
  77. praisonai/cli/features/autonomy_mode.py +490 -0
  78. praisonai/cli/features/background.py +356 -0
  79. praisonai/cli/features/base.py +168 -0
  80. praisonai/cli/features/capabilities.py +1326 -0
  81. praisonai/cli/features/checkpoints.py +338 -0
  82. praisonai/cli/features/code_intelligence.py +652 -0
  83. praisonai/cli/features/compaction.py +294 -0
  84. praisonai/cli/features/compare.py +534 -0
  85. praisonai/cli/features/cost_tracker.py +514 -0
  86. praisonai/cli/features/debug.py +810 -0
  87. praisonai/cli/features/deploy.py +517 -0
  88. praisonai/cli/features/diag.py +289 -0
  89. praisonai/cli/features/doctor/__init__.py +63 -0
  90. praisonai/cli/features/doctor/checks/__init__.py +24 -0
  91. praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
  92. praisonai/cli/features/doctor/checks/config_checks.py +366 -0
  93. praisonai/cli/features/doctor/checks/db_checks.py +366 -0
  94. praisonai/cli/features/doctor/checks/env_checks.py +543 -0
  95. praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
  96. praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
  97. praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
  98. praisonai/cli/features/doctor/checks/network_checks.py +251 -0
  99. praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
  100. praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
  101. praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
  102. praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
  103. praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
  104. praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
  105. praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
  106. praisonai/cli/features/doctor/engine.py +266 -0
  107. praisonai/cli/features/doctor/formatters.py +310 -0
  108. praisonai/cli/features/doctor/handler.py +397 -0
  109. praisonai/cli/features/doctor/models.py +264 -0
  110. praisonai/cli/features/doctor/registry.py +239 -0
  111. praisonai/cli/features/endpoints.py +1019 -0
  112. praisonai/cli/features/eval.py +560 -0
  113. praisonai/cli/features/external_agents.py +231 -0
  114. praisonai/cli/features/fast_context.py +410 -0
  115. praisonai/cli/features/flow_display.py +566 -0
  116. praisonai/cli/features/git_integration.py +651 -0
  117. praisonai/cli/features/guardrail.py +171 -0
  118. praisonai/cli/features/handoff.py +185 -0
  119. praisonai/cli/features/hooks.py +583 -0
  120. praisonai/cli/features/image.py +384 -0
  121. praisonai/cli/features/interactive_runtime.py +585 -0
  122. praisonai/cli/features/interactive_tools.py +380 -0
  123. praisonai/cli/features/interactive_tui.py +603 -0
  124. praisonai/cli/features/jobs.py +632 -0
  125. praisonai/cli/features/knowledge.py +531 -0
  126. praisonai/cli/features/lite.py +244 -0
  127. praisonai/cli/features/lsp_cli.py +225 -0
  128. praisonai/cli/features/mcp.py +169 -0
  129. praisonai/cli/features/message_queue.py +587 -0
  130. praisonai/cli/features/metrics.py +211 -0
  131. praisonai/cli/features/n8n.py +673 -0
  132. praisonai/cli/features/observability.py +293 -0
  133. praisonai/cli/features/ollama.py +361 -0
  134. praisonai/cli/features/output_style.py +273 -0
  135. praisonai/cli/features/package.py +631 -0
  136. praisonai/cli/features/performance.py +308 -0
  137. praisonai/cli/features/persistence.py +636 -0
  138. praisonai/cli/features/profile.py +226 -0
  139. praisonai/cli/features/profiler/__init__.py +81 -0
  140. praisonai/cli/features/profiler/core.py +558 -0
  141. praisonai/cli/features/profiler/optimizations.py +652 -0
  142. praisonai/cli/features/profiler/suite.py +386 -0
  143. praisonai/cli/features/profiling.py +350 -0
  144. praisonai/cli/features/queue/__init__.py +73 -0
  145. praisonai/cli/features/queue/manager.py +395 -0
  146. praisonai/cli/features/queue/models.py +286 -0
  147. praisonai/cli/features/queue/persistence.py +564 -0
  148. praisonai/cli/features/queue/scheduler.py +484 -0
  149. praisonai/cli/features/queue/worker.py +372 -0
  150. praisonai/cli/features/recipe.py +1723 -0
  151. praisonai/cli/features/recipes.py +449 -0
  152. praisonai/cli/features/registry.py +229 -0
  153. praisonai/cli/features/repo_map.py +860 -0
  154. praisonai/cli/features/router.py +466 -0
  155. praisonai/cli/features/sandbox_executor.py +515 -0
  156. praisonai/cli/features/serve.py +829 -0
  157. praisonai/cli/features/session.py +222 -0
  158. praisonai/cli/features/skills.py +856 -0
  159. praisonai/cli/features/slash_commands.py +650 -0
  160. praisonai/cli/features/telemetry.py +179 -0
  161. praisonai/cli/features/templates.py +1384 -0
  162. praisonai/cli/features/thinking.py +305 -0
  163. praisonai/cli/features/todo.py +334 -0
  164. praisonai/cli/features/tools.py +680 -0
  165. praisonai/cli/features/tui/__init__.py +83 -0
  166. praisonai/cli/features/tui/app.py +580 -0
  167. praisonai/cli/features/tui/cli.py +566 -0
  168. praisonai/cli/features/tui/debug.py +511 -0
  169. praisonai/cli/features/tui/events.py +99 -0
  170. praisonai/cli/features/tui/mock_provider.py +328 -0
  171. praisonai/cli/features/tui/orchestrator.py +652 -0
  172. praisonai/cli/features/tui/screens/__init__.py +50 -0
  173. praisonai/cli/features/tui/screens/main.py +245 -0
  174. praisonai/cli/features/tui/screens/queue.py +174 -0
  175. praisonai/cli/features/tui/screens/session.py +124 -0
  176. praisonai/cli/features/tui/screens/settings.py +148 -0
  177. praisonai/cli/features/tui/widgets/__init__.py +56 -0
  178. praisonai/cli/features/tui/widgets/chat.py +261 -0
  179. praisonai/cli/features/tui/widgets/composer.py +224 -0
  180. praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
  181. praisonai/cli/features/tui/widgets/status.py +167 -0
  182. praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
  183. praisonai/cli/features/workflow.py +720 -0
  184. praisonai/cli/legacy.py +236 -0
  185. praisonai/cli/main.py +5559 -0
  186. praisonai/cli/schedule_cli.py +54 -0
  187. praisonai/cli/state/__init__.py +31 -0
  188. praisonai/cli/state/identifiers.py +161 -0
  189. praisonai/cli/state/sessions.py +313 -0
  190. praisonai/code/__init__.py +93 -0
  191. praisonai/code/agent_tools.py +344 -0
  192. praisonai/code/diff/__init__.py +21 -0
  193. praisonai/code/diff/diff_strategy.py +432 -0
  194. praisonai/code/tools/__init__.py +27 -0
  195. praisonai/code/tools/apply_diff.py +221 -0
  196. praisonai/code/tools/execute_command.py +275 -0
  197. praisonai/code/tools/list_files.py +274 -0
  198. praisonai/code/tools/read_file.py +206 -0
  199. praisonai/code/tools/search_replace.py +248 -0
  200. praisonai/code/tools/write_file.py +217 -0
  201. praisonai/code/utils/__init__.py +46 -0
  202. praisonai/code/utils/file_utils.py +307 -0
  203. praisonai/code/utils/ignore_utils.py +308 -0
  204. praisonai/code/utils/text_utils.py +276 -0
  205. praisonai/db/__init__.py +64 -0
  206. praisonai/db/adapter.py +531 -0
  207. praisonai/deploy/__init__.py +62 -0
  208. praisonai/deploy/api.py +231 -0
  209. praisonai/deploy/docker.py +454 -0
  210. praisonai/deploy/doctor.py +367 -0
  211. praisonai/deploy/main.py +327 -0
  212. praisonai/deploy/models.py +179 -0
  213. praisonai/deploy/providers/__init__.py +33 -0
  214. praisonai/deploy/providers/aws.py +331 -0
  215. praisonai/deploy/providers/azure.py +358 -0
  216. praisonai/deploy/providers/base.py +101 -0
  217. praisonai/deploy/providers/gcp.py +314 -0
  218. praisonai/deploy/schema.py +208 -0
  219. praisonai/deploy.py +185 -0
  220. praisonai/endpoints/__init__.py +53 -0
  221. praisonai/endpoints/a2u_server.py +410 -0
  222. praisonai/endpoints/discovery.py +165 -0
  223. praisonai/endpoints/providers/__init__.py +28 -0
  224. praisonai/endpoints/providers/a2a.py +253 -0
  225. praisonai/endpoints/providers/a2u.py +208 -0
  226. praisonai/endpoints/providers/agents_api.py +171 -0
  227. praisonai/endpoints/providers/base.py +231 -0
  228. praisonai/endpoints/providers/mcp.py +263 -0
  229. praisonai/endpoints/providers/recipe.py +206 -0
  230. praisonai/endpoints/providers/tools_mcp.py +150 -0
  231. praisonai/endpoints/registry.py +131 -0
  232. praisonai/endpoints/server.py +161 -0
  233. praisonai/inbuilt_tools/__init__.py +24 -0
  234. praisonai/inbuilt_tools/autogen_tools.py +117 -0
  235. praisonai/inc/__init__.py +2 -0
  236. praisonai/inc/config.py +96 -0
  237. praisonai/inc/models.py +155 -0
  238. praisonai/integrations/__init__.py +56 -0
  239. praisonai/integrations/base.py +303 -0
  240. praisonai/integrations/claude_code.py +270 -0
  241. praisonai/integrations/codex_cli.py +255 -0
  242. praisonai/integrations/cursor_cli.py +195 -0
  243. praisonai/integrations/gemini_cli.py +222 -0
  244. praisonai/jobs/__init__.py +67 -0
  245. praisonai/jobs/executor.py +425 -0
  246. praisonai/jobs/models.py +230 -0
  247. praisonai/jobs/router.py +314 -0
  248. praisonai/jobs/server.py +186 -0
  249. praisonai/jobs/store.py +203 -0
  250. praisonai/llm/__init__.py +66 -0
  251. praisonai/llm/registry.py +382 -0
  252. praisonai/mcp_server/__init__.py +152 -0
  253. praisonai/mcp_server/adapters/__init__.py +74 -0
  254. praisonai/mcp_server/adapters/agents.py +128 -0
  255. praisonai/mcp_server/adapters/capabilities.py +168 -0
  256. praisonai/mcp_server/adapters/cli_tools.py +568 -0
  257. praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
  258. praisonai/mcp_server/adapters/knowledge.py +93 -0
  259. praisonai/mcp_server/adapters/memory.py +104 -0
  260. praisonai/mcp_server/adapters/prompts.py +306 -0
  261. praisonai/mcp_server/adapters/resources.py +124 -0
  262. praisonai/mcp_server/adapters/tools_bridge.py +280 -0
  263. praisonai/mcp_server/auth/__init__.py +48 -0
  264. praisonai/mcp_server/auth/api_key.py +291 -0
  265. praisonai/mcp_server/auth/oauth.py +460 -0
  266. praisonai/mcp_server/auth/oidc.py +289 -0
  267. praisonai/mcp_server/auth/scopes.py +260 -0
  268. praisonai/mcp_server/cli.py +852 -0
  269. praisonai/mcp_server/elicitation.py +445 -0
  270. praisonai/mcp_server/icons.py +302 -0
  271. praisonai/mcp_server/recipe_adapter.py +573 -0
  272. praisonai/mcp_server/recipe_cli.py +824 -0
  273. praisonai/mcp_server/registry.py +703 -0
  274. praisonai/mcp_server/sampling.py +422 -0
  275. praisonai/mcp_server/server.py +490 -0
  276. praisonai/mcp_server/tasks.py +443 -0
  277. praisonai/mcp_server/transports/__init__.py +18 -0
  278. praisonai/mcp_server/transports/http_stream.py +376 -0
  279. praisonai/mcp_server/transports/stdio.py +132 -0
  280. praisonai/persistence/__init__.py +84 -0
  281. praisonai/persistence/config.py +238 -0
  282. praisonai/persistence/conversation/__init__.py +25 -0
  283. praisonai/persistence/conversation/async_mysql.py +427 -0
  284. praisonai/persistence/conversation/async_postgres.py +410 -0
  285. praisonai/persistence/conversation/async_sqlite.py +371 -0
  286. praisonai/persistence/conversation/base.py +151 -0
  287. praisonai/persistence/conversation/json_store.py +250 -0
  288. praisonai/persistence/conversation/mysql.py +387 -0
  289. praisonai/persistence/conversation/postgres.py +401 -0
  290. praisonai/persistence/conversation/singlestore.py +240 -0
  291. praisonai/persistence/conversation/sqlite.py +341 -0
  292. praisonai/persistence/conversation/supabase.py +203 -0
  293. praisonai/persistence/conversation/surrealdb.py +287 -0
  294. praisonai/persistence/factory.py +301 -0
  295. praisonai/persistence/hooks/__init__.py +18 -0
  296. praisonai/persistence/hooks/agent_hooks.py +297 -0
  297. praisonai/persistence/knowledge/__init__.py +26 -0
  298. praisonai/persistence/knowledge/base.py +144 -0
  299. praisonai/persistence/knowledge/cassandra.py +232 -0
  300. praisonai/persistence/knowledge/chroma.py +295 -0
  301. praisonai/persistence/knowledge/clickhouse.py +242 -0
  302. praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
  303. praisonai/persistence/knowledge/couchbase.py +286 -0
  304. praisonai/persistence/knowledge/lancedb.py +216 -0
  305. praisonai/persistence/knowledge/langchain_adapter.py +291 -0
  306. praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
  307. praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
  308. praisonai/persistence/knowledge/milvus.py +277 -0
  309. praisonai/persistence/knowledge/mongodb_vector.py +306 -0
  310. praisonai/persistence/knowledge/pgvector.py +335 -0
  311. praisonai/persistence/knowledge/pinecone.py +253 -0
  312. praisonai/persistence/knowledge/qdrant.py +301 -0
  313. praisonai/persistence/knowledge/redis_vector.py +291 -0
  314. praisonai/persistence/knowledge/singlestore_vector.py +299 -0
  315. praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
  316. praisonai/persistence/knowledge/upstash_vector.py +266 -0
  317. praisonai/persistence/knowledge/weaviate.py +223 -0
  318. praisonai/persistence/migrations/__init__.py +10 -0
  319. praisonai/persistence/migrations/manager.py +251 -0
  320. praisonai/persistence/orchestrator.py +406 -0
  321. praisonai/persistence/state/__init__.py +21 -0
  322. praisonai/persistence/state/async_mongodb.py +200 -0
  323. praisonai/persistence/state/base.py +107 -0
  324. praisonai/persistence/state/dynamodb.py +226 -0
  325. praisonai/persistence/state/firestore.py +175 -0
  326. praisonai/persistence/state/gcs.py +155 -0
  327. praisonai/persistence/state/memory.py +245 -0
  328. praisonai/persistence/state/mongodb.py +158 -0
  329. praisonai/persistence/state/redis.py +190 -0
  330. praisonai/persistence/state/upstash.py +144 -0
  331. praisonai/persistence/tests/__init__.py +3 -0
  332. praisonai/persistence/tests/test_all_backends.py +633 -0
  333. praisonai/profiler.py +1214 -0
  334. praisonai/recipe/__init__.py +134 -0
  335. praisonai/recipe/bridge.py +278 -0
  336. praisonai/recipe/core.py +893 -0
  337. praisonai/recipe/exceptions.py +54 -0
  338. praisonai/recipe/history.py +402 -0
  339. praisonai/recipe/models.py +266 -0
  340. praisonai/recipe/operations.py +440 -0
  341. praisonai/recipe/policy.py +422 -0
  342. praisonai/recipe/registry.py +849 -0
  343. praisonai/recipe/runtime.py +214 -0
  344. praisonai/recipe/security.py +711 -0
  345. praisonai/recipe/serve.py +859 -0
  346. praisonai/recipe/server.py +613 -0
  347. praisonai/scheduler/__init__.py +45 -0
  348. praisonai/scheduler/agent_scheduler.py +552 -0
  349. praisonai/scheduler/base.py +124 -0
  350. praisonai/scheduler/daemon_manager.py +225 -0
  351. praisonai/scheduler/state_manager.py +155 -0
  352. praisonai/scheduler/yaml_loader.py +193 -0
  353. praisonai/scheduler.py +194 -0
  354. praisonai/setup/__init__.py +1 -0
  355. praisonai/setup/build.py +21 -0
  356. praisonai/setup/post_install.py +23 -0
  357. praisonai/setup/setup_conda_env.py +25 -0
  358. praisonai/setup.py +16 -0
  359. praisonai/templates/__init__.py +116 -0
  360. praisonai/templates/cache.py +364 -0
  361. praisonai/templates/dependency_checker.py +358 -0
  362. praisonai/templates/discovery.py +391 -0
  363. praisonai/templates/loader.py +564 -0
  364. praisonai/templates/registry.py +511 -0
  365. praisonai/templates/resolver.py +206 -0
  366. praisonai/templates/security.py +327 -0
  367. praisonai/templates/tool_override.py +498 -0
  368. praisonai/templates/tools_doctor.py +256 -0
  369. praisonai/test.py +105 -0
  370. praisonai/train.py +562 -0
  371. praisonai/train_vision.py +306 -0
  372. praisonai/ui/agents.py +824 -0
  373. praisonai/ui/callbacks.py +57 -0
  374. praisonai/ui/chainlit_compat.py +246 -0
  375. praisonai/ui/chat.py +532 -0
  376. praisonai/ui/code.py +717 -0
  377. praisonai/ui/colab.py +474 -0
  378. praisonai/ui/colab_chainlit.py +81 -0
  379. praisonai/ui/components/aicoder.py +284 -0
  380. praisonai/ui/context.py +283 -0
  381. praisonai/ui/database_config.py +56 -0
  382. praisonai/ui/db.py +294 -0
  383. praisonai/ui/realtime.py +488 -0
  384. praisonai/ui/realtimeclient/__init__.py +756 -0
  385. praisonai/ui/realtimeclient/tools.py +242 -0
  386. praisonai/ui/sql_alchemy.py +710 -0
  387. praisonai/upload_vision.py +140 -0
  388. praisonai/version.py +1 -0
  389. praisonai-3.0.0.dist-info/METADATA +3493 -0
  390. praisonai-3.0.0.dist-info/RECORD +393 -0
  391. praisonai-3.0.0.dist-info/WHEEL +5 -0
  392. praisonai-3.0.0.dist-info/entry_points.txt +4 -0
  393. 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)