agno 2.2.13__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 (575) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +51 -0
  3. agno/agent/agent.py +10405 -0
  4. agno/api/__init__.py +0 -0
  5. agno/api/agent.py +28 -0
  6. agno/api/api.py +40 -0
  7. agno/api/evals.py +22 -0
  8. agno/api/os.py +17 -0
  9. agno/api/routes.py +13 -0
  10. agno/api/schemas/__init__.py +9 -0
  11. agno/api/schemas/agent.py +16 -0
  12. agno/api/schemas/evals.py +16 -0
  13. agno/api/schemas/os.py +14 -0
  14. agno/api/schemas/response.py +6 -0
  15. agno/api/schemas/team.py +16 -0
  16. agno/api/schemas/utils.py +21 -0
  17. agno/api/schemas/workflows.py +16 -0
  18. agno/api/settings.py +53 -0
  19. agno/api/team.py +30 -0
  20. agno/api/workflow.py +28 -0
  21. agno/cloud/aws/base.py +214 -0
  22. agno/cloud/aws/s3/__init__.py +2 -0
  23. agno/cloud/aws/s3/api_client.py +43 -0
  24. agno/cloud/aws/s3/bucket.py +195 -0
  25. agno/cloud/aws/s3/object.py +57 -0
  26. agno/culture/__init__.py +3 -0
  27. agno/culture/manager.py +956 -0
  28. agno/db/__init__.py +24 -0
  29. agno/db/async_postgres/__init__.py +3 -0
  30. agno/db/base.py +598 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2042 -0
  33. agno/db/dynamo/schemas.py +314 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +1795 -0
  37. agno/db/firestore/schemas.py +140 -0
  38. agno/db/firestore/utils.py +376 -0
  39. agno/db/gcs_json/__init__.py +3 -0
  40. agno/db/gcs_json/gcs_json_db.py +1335 -0
  41. agno/db/gcs_json/utils.py +228 -0
  42. agno/db/in_memory/__init__.py +3 -0
  43. agno/db/in_memory/in_memory_db.py +1160 -0
  44. agno/db/in_memory/utils.py +230 -0
  45. agno/db/json/__init__.py +3 -0
  46. agno/db/json/json_db.py +1328 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/__init__.py +0 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/mongo/__init__.py +17 -0
  51. agno/db/mongo/async_mongo.py +2026 -0
  52. agno/db/mongo/mongo.py +1982 -0
  53. agno/db/mongo/schemas.py +87 -0
  54. agno/db/mongo/utils.py +259 -0
  55. agno/db/mysql/__init__.py +3 -0
  56. agno/db/mysql/mysql.py +2308 -0
  57. agno/db/mysql/schemas.py +138 -0
  58. agno/db/mysql/utils.py +355 -0
  59. agno/db/postgres/__init__.py +4 -0
  60. agno/db/postgres/async_postgres.py +1927 -0
  61. agno/db/postgres/postgres.py +2260 -0
  62. agno/db/postgres/schemas.py +139 -0
  63. agno/db/postgres/utils.py +442 -0
  64. agno/db/redis/__init__.py +3 -0
  65. agno/db/redis/redis.py +1660 -0
  66. agno/db/redis/schemas.py +123 -0
  67. agno/db/redis/utils.py +346 -0
  68. agno/db/schemas/__init__.py +4 -0
  69. agno/db/schemas/culture.py +120 -0
  70. agno/db/schemas/evals.py +33 -0
  71. agno/db/schemas/knowledge.py +40 -0
  72. agno/db/schemas/memory.py +46 -0
  73. agno/db/schemas/metrics.py +0 -0
  74. agno/db/singlestore/__init__.py +3 -0
  75. agno/db/singlestore/schemas.py +130 -0
  76. agno/db/singlestore/singlestore.py +2272 -0
  77. agno/db/singlestore/utils.py +384 -0
  78. agno/db/sqlite/__init__.py +4 -0
  79. agno/db/sqlite/async_sqlite.py +2293 -0
  80. agno/db/sqlite/schemas.py +133 -0
  81. agno/db/sqlite/sqlite.py +2288 -0
  82. agno/db/sqlite/utils.py +431 -0
  83. agno/db/surrealdb/__init__.py +3 -0
  84. agno/db/surrealdb/metrics.py +292 -0
  85. agno/db/surrealdb/models.py +309 -0
  86. agno/db/surrealdb/queries.py +71 -0
  87. agno/db/surrealdb/surrealdb.py +1353 -0
  88. agno/db/surrealdb/utils.py +147 -0
  89. agno/db/utils.py +116 -0
  90. agno/debug.py +18 -0
  91. agno/eval/__init__.py +14 -0
  92. agno/eval/accuracy.py +834 -0
  93. agno/eval/performance.py +773 -0
  94. agno/eval/reliability.py +306 -0
  95. agno/eval/utils.py +119 -0
  96. agno/exceptions.py +161 -0
  97. agno/filters.py +354 -0
  98. agno/guardrails/__init__.py +6 -0
  99. agno/guardrails/base.py +19 -0
  100. agno/guardrails/openai.py +144 -0
  101. agno/guardrails/pii.py +94 -0
  102. agno/guardrails/prompt_injection.py +52 -0
  103. agno/integrations/__init__.py +0 -0
  104. agno/integrations/discord/__init__.py +3 -0
  105. agno/integrations/discord/client.py +203 -0
  106. agno/knowledge/__init__.py +5 -0
  107. agno/knowledge/chunking/__init__.py +0 -0
  108. agno/knowledge/chunking/agentic.py +79 -0
  109. agno/knowledge/chunking/document.py +91 -0
  110. agno/knowledge/chunking/fixed.py +57 -0
  111. agno/knowledge/chunking/markdown.py +151 -0
  112. agno/knowledge/chunking/recursive.py +63 -0
  113. agno/knowledge/chunking/row.py +39 -0
  114. agno/knowledge/chunking/semantic.py +86 -0
  115. agno/knowledge/chunking/strategy.py +165 -0
  116. agno/knowledge/content.py +74 -0
  117. agno/knowledge/document/__init__.py +5 -0
  118. agno/knowledge/document/base.py +58 -0
  119. agno/knowledge/embedder/__init__.py +5 -0
  120. agno/knowledge/embedder/aws_bedrock.py +343 -0
  121. agno/knowledge/embedder/azure_openai.py +210 -0
  122. agno/knowledge/embedder/base.py +23 -0
  123. agno/knowledge/embedder/cohere.py +323 -0
  124. agno/knowledge/embedder/fastembed.py +62 -0
  125. agno/knowledge/embedder/fireworks.py +13 -0
  126. agno/knowledge/embedder/google.py +258 -0
  127. agno/knowledge/embedder/huggingface.py +94 -0
  128. agno/knowledge/embedder/jina.py +182 -0
  129. agno/knowledge/embedder/langdb.py +22 -0
  130. agno/knowledge/embedder/mistral.py +206 -0
  131. agno/knowledge/embedder/nebius.py +13 -0
  132. agno/knowledge/embedder/ollama.py +154 -0
  133. agno/knowledge/embedder/openai.py +195 -0
  134. agno/knowledge/embedder/sentence_transformer.py +63 -0
  135. agno/knowledge/embedder/together.py +13 -0
  136. agno/knowledge/embedder/vllm.py +262 -0
  137. agno/knowledge/embedder/voyageai.py +165 -0
  138. agno/knowledge/knowledge.py +1988 -0
  139. agno/knowledge/reader/__init__.py +7 -0
  140. agno/knowledge/reader/arxiv_reader.py +81 -0
  141. agno/knowledge/reader/base.py +95 -0
  142. agno/knowledge/reader/csv_reader.py +166 -0
  143. agno/knowledge/reader/docx_reader.py +82 -0
  144. agno/knowledge/reader/field_labeled_csv_reader.py +292 -0
  145. agno/knowledge/reader/firecrawl_reader.py +201 -0
  146. agno/knowledge/reader/json_reader.py +87 -0
  147. agno/knowledge/reader/markdown_reader.py +137 -0
  148. agno/knowledge/reader/pdf_reader.py +431 -0
  149. agno/knowledge/reader/pptx_reader.py +101 -0
  150. agno/knowledge/reader/reader_factory.py +313 -0
  151. agno/knowledge/reader/s3_reader.py +89 -0
  152. agno/knowledge/reader/tavily_reader.py +194 -0
  153. agno/knowledge/reader/text_reader.py +115 -0
  154. agno/knowledge/reader/web_search_reader.py +372 -0
  155. agno/knowledge/reader/website_reader.py +455 -0
  156. agno/knowledge/reader/wikipedia_reader.py +59 -0
  157. agno/knowledge/reader/youtube_reader.py +78 -0
  158. agno/knowledge/remote_content/__init__.py +0 -0
  159. agno/knowledge/remote_content/remote_content.py +88 -0
  160. agno/knowledge/reranker/__init__.py +3 -0
  161. agno/knowledge/reranker/base.py +14 -0
  162. agno/knowledge/reranker/cohere.py +64 -0
  163. agno/knowledge/reranker/infinity.py +195 -0
  164. agno/knowledge/reranker/sentence_transformer.py +54 -0
  165. agno/knowledge/types.py +39 -0
  166. agno/knowledge/utils.py +189 -0
  167. agno/media.py +462 -0
  168. agno/memory/__init__.py +3 -0
  169. agno/memory/manager.py +1327 -0
  170. agno/models/__init__.py +0 -0
  171. agno/models/aimlapi/__init__.py +5 -0
  172. agno/models/aimlapi/aimlapi.py +45 -0
  173. agno/models/anthropic/__init__.py +5 -0
  174. agno/models/anthropic/claude.py +757 -0
  175. agno/models/aws/__init__.py +15 -0
  176. agno/models/aws/bedrock.py +701 -0
  177. agno/models/aws/claude.py +378 -0
  178. agno/models/azure/__init__.py +18 -0
  179. agno/models/azure/ai_foundry.py +485 -0
  180. agno/models/azure/openai_chat.py +131 -0
  181. agno/models/base.py +2175 -0
  182. agno/models/cerebras/__init__.py +12 -0
  183. agno/models/cerebras/cerebras.py +501 -0
  184. agno/models/cerebras/cerebras_openai.py +112 -0
  185. agno/models/cohere/__init__.py +5 -0
  186. agno/models/cohere/chat.py +389 -0
  187. agno/models/cometapi/__init__.py +5 -0
  188. agno/models/cometapi/cometapi.py +57 -0
  189. agno/models/dashscope/__init__.py +5 -0
  190. agno/models/dashscope/dashscope.py +91 -0
  191. agno/models/deepinfra/__init__.py +5 -0
  192. agno/models/deepinfra/deepinfra.py +28 -0
  193. agno/models/deepseek/__init__.py +5 -0
  194. agno/models/deepseek/deepseek.py +61 -0
  195. agno/models/defaults.py +1 -0
  196. agno/models/fireworks/__init__.py +5 -0
  197. agno/models/fireworks/fireworks.py +26 -0
  198. agno/models/google/__init__.py +5 -0
  199. agno/models/google/gemini.py +1085 -0
  200. agno/models/groq/__init__.py +5 -0
  201. agno/models/groq/groq.py +556 -0
  202. agno/models/huggingface/__init__.py +5 -0
  203. agno/models/huggingface/huggingface.py +491 -0
  204. agno/models/ibm/__init__.py +5 -0
  205. agno/models/ibm/watsonx.py +422 -0
  206. agno/models/internlm/__init__.py +3 -0
  207. agno/models/internlm/internlm.py +26 -0
  208. agno/models/langdb/__init__.py +1 -0
  209. agno/models/langdb/langdb.py +48 -0
  210. agno/models/litellm/__init__.py +14 -0
  211. agno/models/litellm/chat.py +468 -0
  212. agno/models/litellm/litellm_openai.py +25 -0
  213. agno/models/llama_cpp/__init__.py +5 -0
  214. agno/models/llama_cpp/llama_cpp.py +22 -0
  215. agno/models/lmstudio/__init__.py +5 -0
  216. agno/models/lmstudio/lmstudio.py +25 -0
  217. agno/models/message.py +434 -0
  218. agno/models/meta/__init__.py +12 -0
  219. agno/models/meta/llama.py +475 -0
  220. agno/models/meta/llama_openai.py +78 -0
  221. agno/models/metrics.py +120 -0
  222. agno/models/mistral/__init__.py +5 -0
  223. agno/models/mistral/mistral.py +432 -0
  224. agno/models/nebius/__init__.py +3 -0
  225. agno/models/nebius/nebius.py +54 -0
  226. agno/models/nexus/__init__.py +3 -0
  227. agno/models/nexus/nexus.py +22 -0
  228. agno/models/nvidia/__init__.py +5 -0
  229. agno/models/nvidia/nvidia.py +28 -0
  230. agno/models/ollama/__init__.py +5 -0
  231. agno/models/ollama/chat.py +441 -0
  232. agno/models/openai/__init__.py +9 -0
  233. agno/models/openai/chat.py +883 -0
  234. agno/models/openai/like.py +27 -0
  235. agno/models/openai/responses.py +1050 -0
  236. agno/models/openrouter/__init__.py +5 -0
  237. agno/models/openrouter/openrouter.py +66 -0
  238. agno/models/perplexity/__init__.py +5 -0
  239. agno/models/perplexity/perplexity.py +187 -0
  240. agno/models/portkey/__init__.py +3 -0
  241. agno/models/portkey/portkey.py +81 -0
  242. agno/models/requesty/__init__.py +5 -0
  243. agno/models/requesty/requesty.py +52 -0
  244. agno/models/response.py +199 -0
  245. agno/models/sambanova/__init__.py +5 -0
  246. agno/models/sambanova/sambanova.py +28 -0
  247. agno/models/siliconflow/__init__.py +5 -0
  248. agno/models/siliconflow/siliconflow.py +25 -0
  249. agno/models/together/__init__.py +5 -0
  250. agno/models/together/together.py +25 -0
  251. agno/models/utils.py +266 -0
  252. agno/models/vercel/__init__.py +3 -0
  253. agno/models/vercel/v0.py +26 -0
  254. agno/models/vertexai/__init__.py +0 -0
  255. agno/models/vertexai/claude.py +70 -0
  256. agno/models/vllm/__init__.py +3 -0
  257. agno/models/vllm/vllm.py +78 -0
  258. agno/models/xai/__init__.py +3 -0
  259. agno/models/xai/xai.py +113 -0
  260. agno/os/__init__.py +3 -0
  261. agno/os/app.py +876 -0
  262. agno/os/auth.py +57 -0
  263. agno/os/config.py +104 -0
  264. agno/os/interfaces/__init__.py +1 -0
  265. agno/os/interfaces/a2a/__init__.py +3 -0
  266. agno/os/interfaces/a2a/a2a.py +42 -0
  267. agno/os/interfaces/a2a/router.py +250 -0
  268. agno/os/interfaces/a2a/utils.py +924 -0
  269. agno/os/interfaces/agui/__init__.py +3 -0
  270. agno/os/interfaces/agui/agui.py +47 -0
  271. agno/os/interfaces/agui/router.py +144 -0
  272. agno/os/interfaces/agui/utils.py +534 -0
  273. agno/os/interfaces/base.py +25 -0
  274. agno/os/interfaces/slack/__init__.py +3 -0
  275. agno/os/interfaces/slack/router.py +148 -0
  276. agno/os/interfaces/slack/security.py +30 -0
  277. agno/os/interfaces/slack/slack.py +47 -0
  278. agno/os/interfaces/whatsapp/__init__.py +3 -0
  279. agno/os/interfaces/whatsapp/router.py +211 -0
  280. agno/os/interfaces/whatsapp/security.py +53 -0
  281. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  282. agno/os/mcp.py +292 -0
  283. agno/os/middleware/__init__.py +7 -0
  284. agno/os/middleware/jwt.py +233 -0
  285. agno/os/router.py +1763 -0
  286. agno/os/routers/__init__.py +3 -0
  287. agno/os/routers/evals/__init__.py +3 -0
  288. agno/os/routers/evals/evals.py +430 -0
  289. agno/os/routers/evals/schemas.py +142 -0
  290. agno/os/routers/evals/utils.py +162 -0
  291. agno/os/routers/health.py +31 -0
  292. agno/os/routers/home.py +52 -0
  293. agno/os/routers/knowledge/__init__.py +3 -0
  294. agno/os/routers/knowledge/knowledge.py +997 -0
  295. agno/os/routers/knowledge/schemas.py +178 -0
  296. agno/os/routers/memory/__init__.py +3 -0
  297. agno/os/routers/memory/memory.py +515 -0
  298. agno/os/routers/memory/schemas.py +62 -0
  299. agno/os/routers/metrics/__init__.py +3 -0
  300. agno/os/routers/metrics/metrics.py +190 -0
  301. agno/os/routers/metrics/schemas.py +47 -0
  302. agno/os/routers/session/__init__.py +3 -0
  303. agno/os/routers/session/session.py +997 -0
  304. agno/os/schema.py +1055 -0
  305. agno/os/settings.py +43 -0
  306. agno/os/utils.py +630 -0
  307. agno/py.typed +0 -0
  308. agno/reasoning/__init__.py +0 -0
  309. agno/reasoning/anthropic.py +80 -0
  310. agno/reasoning/azure_ai_foundry.py +67 -0
  311. agno/reasoning/deepseek.py +63 -0
  312. agno/reasoning/default.py +97 -0
  313. agno/reasoning/gemini.py +73 -0
  314. agno/reasoning/groq.py +71 -0
  315. agno/reasoning/helpers.py +63 -0
  316. agno/reasoning/ollama.py +67 -0
  317. agno/reasoning/openai.py +86 -0
  318. agno/reasoning/step.py +31 -0
  319. agno/reasoning/vertexai.py +76 -0
  320. agno/run/__init__.py +6 -0
  321. agno/run/agent.py +787 -0
  322. agno/run/base.py +229 -0
  323. agno/run/cancel.py +81 -0
  324. agno/run/messages.py +32 -0
  325. agno/run/team.py +753 -0
  326. agno/run/workflow.py +708 -0
  327. agno/session/__init__.py +10 -0
  328. agno/session/agent.py +295 -0
  329. agno/session/summary.py +265 -0
  330. agno/session/team.py +392 -0
  331. agno/session/workflow.py +205 -0
  332. agno/team/__init__.py +37 -0
  333. agno/team/team.py +8793 -0
  334. agno/tools/__init__.py +10 -0
  335. agno/tools/agentql.py +120 -0
  336. agno/tools/airflow.py +69 -0
  337. agno/tools/api.py +122 -0
  338. agno/tools/apify.py +314 -0
  339. agno/tools/arxiv.py +127 -0
  340. agno/tools/aws_lambda.py +53 -0
  341. agno/tools/aws_ses.py +66 -0
  342. agno/tools/baidusearch.py +89 -0
  343. agno/tools/bitbucket.py +292 -0
  344. agno/tools/brandfetch.py +213 -0
  345. agno/tools/bravesearch.py +106 -0
  346. agno/tools/brightdata.py +367 -0
  347. agno/tools/browserbase.py +209 -0
  348. agno/tools/calcom.py +255 -0
  349. agno/tools/calculator.py +151 -0
  350. agno/tools/cartesia.py +187 -0
  351. agno/tools/clickup.py +244 -0
  352. agno/tools/confluence.py +240 -0
  353. agno/tools/crawl4ai.py +158 -0
  354. agno/tools/csv_toolkit.py +185 -0
  355. agno/tools/dalle.py +110 -0
  356. agno/tools/daytona.py +475 -0
  357. agno/tools/decorator.py +262 -0
  358. agno/tools/desi_vocal.py +108 -0
  359. agno/tools/discord.py +161 -0
  360. agno/tools/docker.py +716 -0
  361. agno/tools/duckdb.py +379 -0
  362. agno/tools/duckduckgo.py +91 -0
  363. agno/tools/e2b.py +703 -0
  364. agno/tools/eleven_labs.py +196 -0
  365. agno/tools/email.py +67 -0
  366. agno/tools/evm.py +129 -0
  367. agno/tools/exa.py +396 -0
  368. agno/tools/fal.py +127 -0
  369. agno/tools/file.py +240 -0
  370. agno/tools/file_generation.py +350 -0
  371. agno/tools/financial_datasets.py +288 -0
  372. agno/tools/firecrawl.py +143 -0
  373. agno/tools/function.py +1187 -0
  374. agno/tools/giphy.py +93 -0
  375. agno/tools/github.py +1760 -0
  376. agno/tools/gmail.py +922 -0
  377. agno/tools/google_bigquery.py +117 -0
  378. agno/tools/google_drive.py +270 -0
  379. agno/tools/google_maps.py +253 -0
  380. agno/tools/googlecalendar.py +674 -0
  381. agno/tools/googlesearch.py +98 -0
  382. agno/tools/googlesheets.py +377 -0
  383. agno/tools/hackernews.py +77 -0
  384. agno/tools/jina.py +101 -0
  385. agno/tools/jira.py +170 -0
  386. agno/tools/knowledge.py +218 -0
  387. agno/tools/linear.py +426 -0
  388. agno/tools/linkup.py +58 -0
  389. agno/tools/local_file_system.py +90 -0
  390. agno/tools/lumalab.py +183 -0
  391. agno/tools/mcp/__init__.py +10 -0
  392. agno/tools/mcp/mcp.py +331 -0
  393. agno/tools/mcp/multi_mcp.py +347 -0
  394. agno/tools/mcp/params.py +24 -0
  395. agno/tools/mcp_toolbox.py +284 -0
  396. agno/tools/mem0.py +193 -0
  397. agno/tools/memori.py +339 -0
  398. agno/tools/memory.py +419 -0
  399. agno/tools/mlx_transcribe.py +139 -0
  400. agno/tools/models/__init__.py +0 -0
  401. agno/tools/models/azure_openai.py +190 -0
  402. agno/tools/models/gemini.py +203 -0
  403. agno/tools/models/groq.py +158 -0
  404. agno/tools/models/morph.py +186 -0
  405. agno/tools/models/nebius.py +124 -0
  406. agno/tools/models_labs.py +195 -0
  407. agno/tools/moviepy_video.py +349 -0
  408. agno/tools/neo4j.py +134 -0
  409. agno/tools/newspaper.py +46 -0
  410. agno/tools/newspaper4k.py +93 -0
  411. agno/tools/notion.py +204 -0
  412. agno/tools/openai.py +202 -0
  413. agno/tools/openbb.py +160 -0
  414. agno/tools/opencv.py +321 -0
  415. agno/tools/openweather.py +233 -0
  416. agno/tools/oxylabs.py +385 -0
  417. agno/tools/pandas.py +102 -0
  418. agno/tools/parallel.py +314 -0
  419. agno/tools/postgres.py +257 -0
  420. agno/tools/pubmed.py +188 -0
  421. agno/tools/python.py +205 -0
  422. agno/tools/reasoning.py +283 -0
  423. agno/tools/reddit.py +467 -0
  424. agno/tools/replicate.py +117 -0
  425. agno/tools/resend.py +62 -0
  426. agno/tools/scrapegraph.py +222 -0
  427. agno/tools/searxng.py +152 -0
  428. agno/tools/serpapi.py +116 -0
  429. agno/tools/serper.py +255 -0
  430. agno/tools/shell.py +53 -0
  431. agno/tools/slack.py +136 -0
  432. agno/tools/sleep.py +20 -0
  433. agno/tools/spider.py +116 -0
  434. agno/tools/sql.py +154 -0
  435. agno/tools/streamlit/__init__.py +0 -0
  436. agno/tools/streamlit/components.py +113 -0
  437. agno/tools/tavily.py +254 -0
  438. agno/tools/telegram.py +48 -0
  439. agno/tools/todoist.py +218 -0
  440. agno/tools/tool_registry.py +1 -0
  441. agno/tools/toolkit.py +146 -0
  442. agno/tools/trafilatura.py +388 -0
  443. agno/tools/trello.py +274 -0
  444. agno/tools/twilio.py +186 -0
  445. agno/tools/user_control_flow.py +78 -0
  446. agno/tools/valyu.py +228 -0
  447. agno/tools/visualization.py +467 -0
  448. agno/tools/webbrowser.py +28 -0
  449. agno/tools/webex.py +76 -0
  450. agno/tools/website.py +54 -0
  451. agno/tools/webtools.py +45 -0
  452. agno/tools/whatsapp.py +286 -0
  453. agno/tools/wikipedia.py +63 -0
  454. agno/tools/workflow.py +278 -0
  455. agno/tools/x.py +335 -0
  456. agno/tools/yfinance.py +257 -0
  457. agno/tools/youtube.py +184 -0
  458. agno/tools/zendesk.py +82 -0
  459. agno/tools/zep.py +454 -0
  460. agno/tools/zoom.py +382 -0
  461. agno/utils/__init__.py +0 -0
  462. agno/utils/agent.py +820 -0
  463. agno/utils/audio.py +49 -0
  464. agno/utils/certs.py +27 -0
  465. agno/utils/code_execution.py +11 -0
  466. agno/utils/common.py +132 -0
  467. agno/utils/dttm.py +13 -0
  468. agno/utils/enum.py +22 -0
  469. agno/utils/env.py +11 -0
  470. agno/utils/events.py +696 -0
  471. agno/utils/format_str.py +16 -0
  472. agno/utils/functions.py +166 -0
  473. agno/utils/gemini.py +426 -0
  474. agno/utils/hooks.py +57 -0
  475. agno/utils/http.py +74 -0
  476. agno/utils/json_schema.py +234 -0
  477. agno/utils/knowledge.py +36 -0
  478. agno/utils/location.py +19 -0
  479. agno/utils/log.py +255 -0
  480. agno/utils/mcp.py +214 -0
  481. agno/utils/media.py +352 -0
  482. agno/utils/merge_dict.py +41 -0
  483. agno/utils/message.py +118 -0
  484. agno/utils/models/__init__.py +0 -0
  485. agno/utils/models/ai_foundry.py +43 -0
  486. agno/utils/models/claude.py +358 -0
  487. agno/utils/models/cohere.py +87 -0
  488. agno/utils/models/llama.py +78 -0
  489. agno/utils/models/mistral.py +98 -0
  490. agno/utils/models/openai_responses.py +140 -0
  491. agno/utils/models/schema_utils.py +153 -0
  492. agno/utils/models/watsonx.py +41 -0
  493. agno/utils/openai.py +257 -0
  494. agno/utils/pickle.py +32 -0
  495. agno/utils/pprint.py +178 -0
  496. agno/utils/print_response/__init__.py +0 -0
  497. agno/utils/print_response/agent.py +842 -0
  498. agno/utils/print_response/team.py +1724 -0
  499. agno/utils/print_response/workflow.py +1668 -0
  500. agno/utils/prompts.py +111 -0
  501. agno/utils/reasoning.py +108 -0
  502. agno/utils/response.py +163 -0
  503. agno/utils/response_iterator.py +17 -0
  504. agno/utils/safe_formatter.py +24 -0
  505. agno/utils/serialize.py +32 -0
  506. agno/utils/shell.py +22 -0
  507. agno/utils/streamlit.py +487 -0
  508. agno/utils/string.py +231 -0
  509. agno/utils/team.py +139 -0
  510. agno/utils/timer.py +41 -0
  511. agno/utils/tools.py +102 -0
  512. agno/utils/web.py +23 -0
  513. agno/utils/whatsapp.py +305 -0
  514. agno/utils/yaml_io.py +25 -0
  515. agno/vectordb/__init__.py +3 -0
  516. agno/vectordb/base.py +127 -0
  517. agno/vectordb/cassandra/__init__.py +5 -0
  518. agno/vectordb/cassandra/cassandra.py +501 -0
  519. agno/vectordb/cassandra/extra_param_mixin.py +11 -0
  520. agno/vectordb/cassandra/index.py +13 -0
  521. agno/vectordb/chroma/__init__.py +5 -0
  522. agno/vectordb/chroma/chromadb.py +929 -0
  523. agno/vectordb/clickhouse/__init__.py +9 -0
  524. agno/vectordb/clickhouse/clickhousedb.py +835 -0
  525. agno/vectordb/clickhouse/index.py +9 -0
  526. agno/vectordb/couchbase/__init__.py +3 -0
  527. agno/vectordb/couchbase/couchbase.py +1442 -0
  528. agno/vectordb/distance.py +7 -0
  529. agno/vectordb/lancedb/__init__.py +6 -0
  530. agno/vectordb/lancedb/lance_db.py +995 -0
  531. agno/vectordb/langchaindb/__init__.py +5 -0
  532. agno/vectordb/langchaindb/langchaindb.py +163 -0
  533. agno/vectordb/lightrag/__init__.py +5 -0
  534. agno/vectordb/lightrag/lightrag.py +388 -0
  535. agno/vectordb/llamaindex/__init__.py +3 -0
  536. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  537. agno/vectordb/milvus/__init__.py +4 -0
  538. agno/vectordb/milvus/milvus.py +1182 -0
  539. agno/vectordb/mongodb/__init__.py +9 -0
  540. agno/vectordb/mongodb/mongodb.py +1417 -0
  541. agno/vectordb/pgvector/__init__.py +12 -0
  542. agno/vectordb/pgvector/index.py +23 -0
  543. agno/vectordb/pgvector/pgvector.py +1462 -0
  544. agno/vectordb/pineconedb/__init__.py +5 -0
  545. agno/vectordb/pineconedb/pineconedb.py +747 -0
  546. agno/vectordb/qdrant/__init__.py +5 -0
  547. agno/vectordb/qdrant/qdrant.py +1134 -0
  548. agno/vectordb/redis/__init__.py +9 -0
  549. agno/vectordb/redis/redisdb.py +694 -0
  550. agno/vectordb/search.py +7 -0
  551. agno/vectordb/singlestore/__init__.py +10 -0
  552. agno/vectordb/singlestore/index.py +41 -0
  553. agno/vectordb/singlestore/singlestore.py +763 -0
  554. agno/vectordb/surrealdb/__init__.py +3 -0
  555. agno/vectordb/surrealdb/surrealdb.py +699 -0
  556. agno/vectordb/upstashdb/__init__.py +5 -0
  557. agno/vectordb/upstashdb/upstashdb.py +718 -0
  558. agno/vectordb/weaviate/__init__.py +8 -0
  559. agno/vectordb/weaviate/index.py +15 -0
  560. agno/vectordb/weaviate/weaviate.py +1005 -0
  561. agno/workflow/__init__.py +23 -0
  562. agno/workflow/agent.py +299 -0
  563. agno/workflow/condition.py +738 -0
  564. agno/workflow/loop.py +735 -0
  565. agno/workflow/parallel.py +824 -0
  566. agno/workflow/router.py +702 -0
  567. agno/workflow/step.py +1432 -0
  568. agno/workflow/steps.py +592 -0
  569. agno/workflow/types.py +520 -0
  570. agno/workflow/workflow.py +4321 -0
  571. agno-2.2.13.dist-info/METADATA +614 -0
  572. agno-2.2.13.dist-info/RECORD +575 -0
  573. agno-2.2.13.dist-info/WHEEL +5 -0
  574. agno-2.2.13.dist-info/licenses/LICENSE +201 -0
  575. agno-2.2.13.dist-info/top_level.txt +1 -0
agno/tools/github.py ADDED
@@ -0,0 +1,1760 @@
1
+ import json
2
+ from os import getenv
3
+ from typing import Any, List, Optional
4
+
5
+ from agno.tools import Toolkit
6
+ from agno.utils.log import log_debug, logger
7
+
8
+ try:
9
+ from github import Auth, Github, GithubException
10
+
11
+ except ImportError:
12
+ raise ImportError("`PyGithub` not installed. Please install using `pip install pygithub`")
13
+
14
+
15
+ class GithubTools(Toolkit):
16
+ def __init__(
17
+ self,
18
+ access_token: Optional[str] = None,
19
+ base_url: Optional[str] = None,
20
+ **kwargs,
21
+ ):
22
+ self.access_token = access_token or getenv("GITHUB_ACCESS_TOKEN")
23
+ self.base_url = base_url
24
+
25
+ self.g = self.authenticate()
26
+
27
+ tools: List[Any] = [
28
+ self.search_repositories,
29
+ self.list_repositories,
30
+ self.get_repository,
31
+ self.get_pull_request,
32
+ self.get_pull_request_changes,
33
+ self.create_issue,
34
+ self.create_repository,
35
+ self.delete_repository,
36
+ self.list_branches,
37
+ self.get_repository_languages,
38
+ self.get_pull_request_count,
39
+ self.get_repository_stars,
40
+ self.get_pull_requests,
41
+ self.get_pull_request_comments,
42
+ self.create_pull_request_comment,
43
+ self.edit_pull_request_comment,
44
+ self.get_pull_request_with_details,
45
+ self.get_repository_with_stats,
46
+ self.list_issues,
47
+ self.get_issue,
48
+ self.comment_on_issue,
49
+ self.close_issue,
50
+ self.reopen_issue,
51
+ self.assign_issue,
52
+ self.label_issue,
53
+ self.list_issue_comments,
54
+ self.edit_issue,
55
+ self.create_pull_request,
56
+ self.create_file,
57
+ self.get_file_content,
58
+ self.update_file,
59
+ self.delete_file,
60
+ self.get_directory_content,
61
+ self.get_branch_content,
62
+ self.create_branch,
63
+ self.set_default_branch,
64
+ self.search_code,
65
+ self.search_issues_and_prs,
66
+ self.create_review_request,
67
+ ]
68
+
69
+ super().__init__(name="github", tools=tools, **kwargs)
70
+
71
+ def authenticate(self):
72
+ """Authenticate with GitHub using the provided access token."""
73
+ if not self.access_token: # Fixes lint type error
74
+ raise ValueError("GitHub access token is required")
75
+
76
+ auth = Auth.Token(self.access_token)
77
+ if self.base_url:
78
+ log_debug(f"Authenticating with GitHub Enterprise at {self.base_url}")
79
+ return Github(base_url=self.base_url, auth=auth)
80
+ else:
81
+ log_debug("Authenticating with public GitHub")
82
+ return Github(auth=auth)
83
+
84
+ def search_repositories(
85
+ self,
86
+ query: str,
87
+ sort: str = "stars",
88
+ order: str = "desc",
89
+ page: int = 1,
90
+ per_page: int = 30,
91
+ ) -> str:
92
+ """Search for repositories on GitHub.
93
+
94
+ Note: GitHub's Search API has a maximum limit of 1000 results per query.
95
+
96
+ Args:
97
+ query (str): The search query keywords.
98
+ sort (str, optional): The field to sort results by. Can be 'stars', 'forks', or 'updated'. Defaults to 'stars'.
99
+ order (str, optional): The order of results. Can be 'asc' or 'desc'. Defaults to 'desc'.
100
+ page (int, optional): Page number of results to return, counting from 1. Defaults to 1.
101
+ per_page (int, optional): Number of results per page. Max 100. Defaults to 30.
102
+
103
+ Returns:
104
+ A JSON-formatted string containing a list of repositories matching the search query.
105
+ """
106
+ log_debug(f"Searching repositories with query: '{query}', page: {page}, per_page: {per_page}")
107
+ try:
108
+ # Ensure per_page doesn't exceed GitHub's max of 100
109
+ per_page = min(per_page, 100)
110
+
111
+ repositories = self.g.search_repositories(query=query, sort=sort, order=order)
112
+
113
+ # Get the specified page of results
114
+ repo_list = []
115
+ for repo in repositories.get_page(page - 1):
116
+ repo_info = {
117
+ "full_name": repo.full_name,
118
+ "description": repo.description,
119
+ "url": repo.html_url,
120
+ "stars": repo.stargazers_count,
121
+ "forks": repo.forks_count,
122
+ "language": repo.language,
123
+ }
124
+ repo_list.append(repo_info)
125
+
126
+ if len(repo_list) >= per_page:
127
+ break
128
+
129
+ return json.dumps(repo_list, indent=2)
130
+
131
+ except GithubException as e:
132
+ logger.error(f"Error searching repositories: {e}")
133
+ return json.dumps({"error": str(e)})
134
+
135
+ def list_repositories(self) -> str:
136
+ """List all repositories for the authenticated user.
137
+
138
+ Returns:
139
+ A JSON-formatted string containing a list of repository names.
140
+ """
141
+ log_debug("Listing repositories")
142
+ try:
143
+ repos = self.g.get_user().get_repos()
144
+ repo_names = [repo.full_name for repo in repos]
145
+ return json.dumps(repo_names, indent=2)
146
+ except GithubException as e:
147
+ logger.error(f"Error listing repositories: {e}")
148
+ return json.dumps({"error": str(e)})
149
+
150
+ def create_repository(
151
+ self,
152
+ name: str,
153
+ private: bool = False,
154
+ description: Optional[str] = None,
155
+ auto_init: bool = False,
156
+ organization: Optional[str] = None,
157
+ ) -> str:
158
+ """Create a new repository on GitHub.
159
+
160
+ Args:
161
+ name (str): The name of the repository.
162
+ private (bool, optional): Whether the repository is private. Defaults to False.
163
+ description (str, optional): A short description of the repository.
164
+ auto_init (bool, optional): Whether to initialize the repository with a README. Defaults to False.
165
+ organization (str, optional): Name of organization to create repo in. If None, creates in user account.
166
+
167
+ Returns:
168
+ A JSON-formatted string containing the created repository details.
169
+ """
170
+ log_debug(f"Creating repository: {name}")
171
+ try:
172
+ description = description if description is not None else ""
173
+
174
+ if organization:
175
+ log_debug(f"Creating in organization: {organization}")
176
+ org = self.g.get_organization(organization)
177
+ repo = org.create_repo(
178
+ name=name,
179
+ private=private,
180
+ description=description,
181
+ auto_init=auto_init,
182
+ )
183
+ else:
184
+ repo = self.g.get_user().create_repo(
185
+ name=name,
186
+ private=private,
187
+ description=description,
188
+ auto_init=auto_init,
189
+ )
190
+
191
+ repo_info = {
192
+ "name": repo.full_name,
193
+ "url": repo.html_url,
194
+ "private": repo.private,
195
+ "description": repo.description,
196
+ }
197
+ return json.dumps(repo_info, indent=2)
198
+ except GithubException as e:
199
+ logger.error(f"Error creating repository: {e}")
200
+ return json.dumps({"error": str(e)})
201
+
202
+ def get_repository(self, repo_name: str) -> str:
203
+ """Get details of a specific repository.
204
+
205
+ Args:
206
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
207
+
208
+ Returns:
209
+ A JSON-formatted string containing repository details.
210
+ """
211
+ log_debug(f"Getting repository: {repo_name}")
212
+ try:
213
+ repo = self.g.get_repo(repo_name)
214
+ repo_info = {
215
+ "name": repo.full_name,
216
+ "description": repo.description,
217
+ "url": repo.html_url,
218
+ "stars": repo.stargazers_count,
219
+ "forks": repo.forks_count,
220
+ "open_issues": repo.open_issues_count,
221
+ "language": repo.language,
222
+ "license": repo.license.name if repo.license else None,
223
+ "default_branch": repo.default_branch,
224
+ }
225
+ return json.dumps(repo_info, indent=2)
226
+ except GithubException as e:
227
+ logger.error(f"Error getting repository: {e}")
228
+ return json.dumps({"error": str(e)})
229
+
230
+ def get_repository_languages(self, repo_name: str) -> str:
231
+ """Get the languages used in a repository.
232
+
233
+ Args:
234
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
235
+
236
+ Returns:
237
+ A JSON-formatted string containing the list of languages.
238
+ """
239
+ log_debug(f"Getting languages for repository: {repo_name}")
240
+ try:
241
+ repo = self.g.get_repo(repo_name)
242
+ languages = repo.get_languages()
243
+ return json.dumps(languages, indent=2)
244
+ except GithubException as e:
245
+ logger.error(f"Error getting repository languages: {e}")
246
+ return json.dumps({"error": str(e)})
247
+
248
+ def get_pull_request_count(
249
+ self,
250
+ repo_name: str,
251
+ state: str = "all",
252
+ author: Optional[str] = None,
253
+ base: Optional[str] = None,
254
+ head: Optional[str] = None,
255
+ ) -> str:
256
+ """Get the count of pull requests for a repository based on query parameters.
257
+
258
+ Args:
259
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
260
+ state (str, optional): The state of the PRs to count ('open', 'closed', 'all'). Defaults to 'all'.
261
+ author (str, optional): Filter PRs by author username.
262
+ base (str, optional): Filter PRs by base branch name.
263
+ head (str, optional): Filter PRs by head branch name.
264
+
265
+ Returns:
266
+ A JSON-formatted string containing the count of pull requests.
267
+ """
268
+ log_debug(f"Counting pull requests for repository: {repo_name} with state: {state}")
269
+ try:
270
+ repo = self.g.get_repo(repo_name)
271
+ pulls = repo.get_pulls(state=state, base=base, head=head)
272
+
273
+ # If author is specified, filter the results
274
+ if author:
275
+ # If we need to filter by author and state, make sure both conditions are met
276
+ if state != "all":
277
+ count = sum(1 for pr in pulls if pr.user.login == author and pr.state == state)
278
+ else:
279
+ count = sum(1 for pr in pulls if pr.user.login == author)
280
+ else:
281
+ count = pulls.totalCount
282
+
283
+ return json.dumps({"count": count}, indent=2)
284
+ except GithubException as e:
285
+ logger.error(f"Error counting pull requests: {e}")
286
+ return json.dumps({"error": str(e)})
287
+
288
+ def get_pull_request(self, repo_name: str, pr_number: int) -> str:
289
+ """Get details of a specific pull request.
290
+
291
+ Args:
292
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
293
+ pr_number (int): The number of the pull request.
294
+
295
+
296
+ Returns:
297
+ A JSON-formatted string containing pull request details.
298
+ """
299
+ log_debug(f"Getting pull request #{pr_number} for repository: {repo_name}")
300
+ try:
301
+ repo = self.g.get_repo(repo_name)
302
+ pr = repo.get_pull(pr_number)
303
+ pr_info = {
304
+ "number": pr.number,
305
+ "title": pr.title,
306
+ "user": pr.user.login,
307
+ "body": pr.body,
308
+ "created_at": pr.created_at.isoformat(),
309
+ "updated_at": pr.updated_at.isoformat(),
310
+ "state": pr.state,
311
+ "merged": pr.is_merged(),
312
+ "mergeable": pr.mergeable,
313
+ "url": pr.html_url,
314
+ }
315
+ return json.dumps(pr_info, indent=2)
316
+ except GithubException as e:
317
+ logger.error(f"Error getting pull request: {e}")
318
+ return json.dumps({"error": str(e)})
319
+
320
+ def get_pull_request_changes(self, repo_name: str, pr_number: int) -> str:
321
+ """Get the changes (files modified) in a pull request.
322
+
323
+ Args:
324
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
325
+ pr_number (int): The number of the pull request.
326
+
327
+ Returns:
328
+ A JSON-formatted string containing the list of changed files.
329
+ """
330
+ log_debug(f"Getting changes for pull request #{pr_number} in repository: {repo_name}")
331
+ try:
332
+ repo = self.g.get_repo(repo_name)
333
+ pr = repo.get_pull(pr_number)
334
+ files = pr.get_files()
335
+ changes = []
336
+ for file in files:
337
+ file_info = {
338
+ "filename": file.filename,
339
+ "status": file.status,
340
+ "additions": file.additions,
341
+ "deletions": file.deletions,
342
+ "changes": file.changes,
343
+ "raw_url": file.raw_url,
344
+ "blob_url": file.blob_url,
345
+ "patch": file.patch,
346
+ }
347
+ changes.append(file_info)
348
+ return json.dumps(changes, indent=2)
349
+ except GithubException as e:
350
+ logger.error(f"Error getting pull request changes: {e}")
351
+ return json.dumps({"error": str(e)})
352
+
353
+ def create_issue(self, repo_name: str, title: str, body: Optional[str] = None) -> str:
354
+ """Create an issue in a repository.
355
+
356
+ Args:
357
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
358
+ title (str): The title of the issue.
359
+ body (str, optional): The body content of the issue.
360
+
361
+ Returns:
362
+ A JSON-formatted string containing the created issue details.
363
+ """
364
+ log_debug(f"Creating issue in repository: {repo_name}")
365
+ try:
366
+ repo = self.g.get_repo(repo_name)
367
+ issue = repo.create_issue(title=title, body=body) # type: ignore
368
+ issue_info = {
369
+ "id": issue.id,
370
+ "number": issue.number,
371
+ "title": issue.title,
372
+ "body": issue.body,
373
+ "url": issue.html_url,
374
+ "state": issue.state,
375
+ "created_at": issue.created_at.isoformat(),
376
+ "user": issue.user.login,
377
+ }
378
+ return json.dumps(issue_info, indent=2)
379
+ except GithubException as e:
380
+ logger.error(f"Error creating issue: {e}")
381
+ return json.dumps({"error": str(e)})
382
+
383
+ def list_issues(self, repo_name: str, state: str = "open", page: int = 1, per_page: int = 20) -> str:
384
+ """List issues for a repository with pagination.
385
+
386
+ Args:
387
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
388
+ state (str, optional): The state of issues to list ('open', 'closed', 'all'). Defaults to 'open'.
389
+ page (int, optional): Page number of results to return, counting from 1. Defaults to 1.
390
+ per_page (int, optional): Number of results per page. Defaults to 20.
391
+ Returns:
392
+ A JSON-formatted string containing a list of issues with pagination metadata.
393
+ """
394
+ log_debug(f"Listing issues for repository: {repo_name} with state: {state}, page: {page}, per_page: {per_page}")
395
+ try:
396
+ repo = self.g.get_repo(repo_name)
397
+
398
+ issues = repo.get_issues(state=state)
399
+
400
+ # Filter out pull requests after fetching issues
401
+ total_issues = 0
402
+ all_issues = []
403
+ for issue in issues:
404
+ if not issue.pull_request:
405
+ all_issues.append(issue)
406
+ total_issues += 1
407
+
408
+ # Calculate pagination metadata
409
+ total_pages = (total_issues + per_page - 1) // per_page
410
+
411
+ # Validate page number
412
+ if page < 1:
413
+ page = 1
414
+ elif page > total_pages and total_pages > 0:
415
+ page = total_pages
416
+
417
+ # Get the specified page of results
418
+ issue_list = []
419
+ page_start = (page - 1) * per_page
420
+ page_end = page_start + per_page
421
+
422
+ for i in range(page_start, min(page_end, total_issues)):
423
+ if i < len(all_issues):
424
+ issue = all_issues[i]
425
+ issue_info = {
426
+ "number": issue.number,
427
+ "title": issue.title,
428
+ "user": issue.user.login,
429
+ "created_at": issue.created_at.isoformat(),
430
+ "state": issue.state,
431
+ "url": issue.html_url,
432
+ }
433
+ issue_list.append(issue_info)
434
+
435
+ meta = {"current_page": page, "per_page": per_page, "total_items": total_issues, "total_pages": total_pages}
436
+
437
+ response = {"data": issue_list, "meta": meta}
438
+
439
+ return json.dumps(response, indent=2)
440
+ except GithubException as e:
441
+ logger.error(f"Error listing issues: {e}")
442
+ return json.dumps({"error": str(e)})
443
+
444
+ def get_issue(self, repo_name: str, issue_number: int) -> str:
445
+ """Get details of a specific issue.
446
+
447
+ Args:
448
+ repo_name (str): The full name of the repository.
449
+ issue_number (int): The number of the issue.
450
+
451
+ Returns:
452
+ A JSON-formatted string containing issue details.
453
+ """
454
+ log_debug(f"Getting issue #{issue_number} for repository: {repo_name}")
455
+ try:
456
+ repo = self.g.get_repo(repo_name)
457
+ issue = repo.get_issue(number=issue_number)
458
+ issue_info = {
459
+ "number": issue.number,
460
+ "title": issue.title,
461
+ "body": issue.body,
462
+ "user": issue.user.login,
463
+ "state": issue.state,
464
+ "created_at": issue.created_at.isoformat(),
465
+ "updated_at": issue.updated_at.isoformat(),
466
+ "url": issue.html_url,
467
+ "assignees": [assignee.login for assignee in issue.assignees],
468
+ "labels": [label.name for label in issue.labels],
469
+ }
470
+ return json.dumps(issue_info, indent=2)
471
+ except GithubException as e:
472
+ logger.error(f"Error getting issue: {e}")
473
+ return json.dumps({"error": str(e)})
474
+
475
+ def comment_on_issue(self, repo_name: str, issue_number: int, comment_body: str) -> str:
476
+ """Add a comment to an issue.
477
+
478
+ Args:
479
+ repo_name (str): The full name of the repository.
480
+ issue_number (int): The number of the issue.
481
+ comment_body (str): The content of the comment.
482
+
483
+ Returns:
484
+ A JSON-formatted string containing the comment details.
485
+ """
486
+ log_debug(f"Adding comment to issue #{issue_number} in repository: {repo_name}")
487
+ try:
488
+ repo = self.g.get_repo(repo_name)
489
+ issue = repo.get_issue(number=issue_number)
490
+ comment = issue.create_comment(body=comment_body)
491
+ comment_info = {
492
+ "id": comment.id,
493
+ "body": comment.body,
494
+ "user": comment.user.login,
495
+ "created_at": comment.created_at.isoformat(),
496
+ "url": comment.html_url,
497
+ }
498
+ return json.dumps(comment_info, indent=2)
499
+ except GithubException as e:
500
+ logger.error(f"Error commenting on issue: {e}")
501
+ return json.dumps({"error": str(e)})
502
+
503
+ def close_issue(self, repo_name: str, issue_number: int) -> str:
504
+ """Close an issue.
505
+
506
+ Args:
507
+ repo_name (str): The full name of the repository.
508
+ issue_number (int): The number of the issue.
509
+
510
+ Returns:
511
+ A JSON-formatted string confirming the issue is closed.
512
+ """
513
+ log_debug(f"Closing issue #{issue_number} in repository: {repo_name}")
514
+ try:
515
+ repo = self.g.get_repo(repo_name)
516
+ issue = repo.get_issue(number=issue_number)
517
+ issue.edit(state="closed")
518
+ return json.dumps({"message": f"Issue #{issue_number} closed."}, indent=2)
519
+ except GithubException as e:
520
+ logger.error(f"Error closing issue: {e}")
521
+ return json.dumps({"error": str(e)})
522
+
523
+ def reopen_issue(self, repo_name: str, issue_number: int) -> str:
524
+ """Reopen a closed issue.
525
+
526
+ Args:
527
+ repo_name (str): The full name of the repository.
528
+ issue_number (int): The number of the issue.
529
+
530
+ Returns:
531
+ A JSON-formatted string confirming the issue is reopened.
532
+ """
533
+ log_debug(f"Reopening issue #{issue_number} in repository: {repo_name}")
534
+ try:
535
+ repo = self.g.get_repo(repo_name)
536
+ issue = repo.get_issue(number=issue_number)
537
+ issue.edit(state="open")
538
+ return json.dumps({"message": f"Issue #{issue_number} reopened."}, indent=2)
539
+ except GithubException as e:
540
+ logger.error(f"Error reopening issue: {e}")
541
+ return json.dumps({"error": str(e)})
542
+
543
+ def assign_issue(self, repo_name: str, issue_number: int, assignees: List[str]) -> str:
544
+ """Assign users to an issue.
545
+
546
+ Args:
547
+ repo_name (str): The full name of the repository.
548
+ issue_number (int): The number of the issue.
549
+ assignees (List[str]): A list of usernames to assign.
550
+
551
+ Returns:
552
+ A JSON-formatted string confirming the assignees.
553
+ """
554
+ log_debug(f"Assigning users to issue #{issue_number} in repository: {repo_name}")
555
+ try:
556
+ repo = self.g.get_repo(repo_name)
557
+ issue = repo.get_issue(number=issue_number)
558
+ issue.edit(assignees=assignees)
559
+ return json.dumps({"message": f"Issue #{issue_number} assigned to {assignees}."}, indent=2)
560
+ except GithubException as e:
561
+ logger.error(f"Error assigning issue: {e}")
562
+ return json.dumps({"error": str(e)})
563
+
564
+ def label_issue(self, repo_name: str, issue_number: int, labels: List[str]) -> str:
565
+ """Add labels to an issue.
566
+
567
+ Args:
568
+ repo_name (str): The full name of the repository.
569
+ issue_number (int): The number of the issue.
570
+ labels (List[str]): A list of label names to add.
571
+
572
+ Returns:
573
+ A JSON-formatted string confirming the labels.
574
+ """
575
+ log_debug(f"Labeling issue #{issue_number} in repository: {repo_name}")
576
+ try:
577
+ repo = self.g.get_repo(repo_name)
578
+ issue = repo.get_issue(number=issue_number)
579
+ issue.edit(labels=labels)
580
+ return json.dumps(
581
+ {"message": f"Labels {labels} added to issue #{issue_number}."},
582
+ indent=2,
583
+ )
584
+ except GithubException as e:
585
+ logger.error(f"Error labeling issue: {e}")
586
+ return json.dumps({"error": str(e)})
587
+
588
+ def list_issue_comments(self, repo_name: str, issue_number: int) -> str:
589
+ """List comments on an issue.
590
+
591
+ Args:
592
+ repo_name (str): The full name of the repository.
593
+ issue_number (int): The number of the issue.
594
+
595
+ Returns:
596
+ A JSON-formatted string containing a list of comments.
597
+ """
598
+ log_debug(f"Listing comments for issue #{issue_number} in repository: {repo_name}")
599
+ try:
600
+ repo = self.g.get_repo(repo_name)
601
+ issue = repo.get_issue(number=issue_number)
602
+ comments = issue.get_comments()
603
+ comment_list = []
604
+ for comment in comments:
605
+ comment_info = {
606
+ "id": comment.id,
607
+ "user": comment.user.login,
608
+ "body": comment.body,
609
+ "created_at": comment.created_at.isoformat(),
610
+ "url": comment.html_url,
611
+ }
612
+ comment_list.append(comment_info)
613
+ return json.dumps(comment_list, indent=2)
614
+ except GithubException as e:
615
+ logger.error(f"Error listing issue comments: {e}")
616
+ return json.dumps({"error": str(e)})
617
+
618
+ def edit_issue(
619
+ self,
620
+ repo_name: str,
621
+ issue_number: int,
622
+ title: Optional[str] = None,
623
+ body: Optional[str] = None,
624
+ ) -> str:
625
+ """Edit the title or body of an issue.
626
+
627
+ Args:
628
+ repo_name (str): The full name of the repository.
629
+ issue_number (int): The number of the issue.
630
+ title (str, optional): The new title for the issue.
631
+ body (str, optional): The new body content for the issue.
632
+
633
+ Returns:
634
+ A JSON-formatted string confirming the issue has been updated.
635
+ """
636
+ log_debug(f"Editing issue #{issue_number} in repository: {repo_name}")
637
+ try:
638
+ repo = self.g.get_repo(repo_name)
639
+ issue = repo.get_issue(number=issue_number)
640
+ issue.edit(title=title, body=body) # type: ignore
641
+ return json.dumps({"message": f"Issue #{issue_number} updated."}, indent=2)
642
+ except GithubException as e:
643
+ logger.error(f"Error editing issue: {e}")
644
+ return json.dumps({"error": str(e)})
645
+
646
+ def delete_repository(self, repo_name: str) -> str:
647
+ """Delete a repository (requires admin permissions).
648
+
649
+ Args:
650
+ repo_name (str): The full name of the repository to delete (e.g., 'owner/repo').
651
+
652
+ Returns:
653
+ A JSON-formatted string with success message or error.
654
+ """
655
+ log_debug(f"Deleting repository: {repo_name}")
656
+ try:
657
+ repo = self.g.get_repo(repo_name)
658
+ repo.delete()
659
+ return json.dumps({"message": f"Repository {repo_name} deleted successfully"}, indent=2)
660
+ except GithubException as e:
661
+ logger.error(f"Error deleting repository: {e}")
662
+ return json.dumps({"error": str(e)})
663
+
664
+ def list_branches(self, repo_name: str) -> str:
665
+ """List all branches in a repository.
666
+
667
+ Args:
668
+ repo_name (str): Full repository name (e.g., 'owner/repo').
669
+
670
+ Returns:
671
+ JSON list of branch names.
672
+ """
673
+ try:
674
+ repo = self.g.get_repo(repo_name)
675
+ branches = [branch.name for branch in repo.get_branches()]
676
+ return json.dumps(branches, indent=2)
677
+ except GithubException as e:
678
+ logger.error(f"Error listing branches: {e}")
679
+ return json.dumps({"error": str(e)})
680
+
681
+ def get_repository_stars(self, repo_name: str) -> str:
682
+ """Get the number of stars for a repository.
683
+
684
+ Args:
685
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
686
+
687
+ Returns:
688
+ A JSON-formatted string containing the star count.
689
+ """
690
+ log_debug(f"Getting star count for repository: {repo_name}")
691
+ try:
692
+ repo = self.g.get_repo(repo_name)
693
+ return json.dumps({"stars": repo.stargazers_count}, indent=2)
694
+ except GithubException as e:
695
+ logger.error(f"Error getting repository stars: {e}")
696
+ return json.dumps({"error": str(e)})
697
+
698
+ def get_pull_requests(
699
+ self,
700
+ repo_name: str,
701
+ state: str = "open",
702
+ sort: str = "created",
703
+ direction: str = "desc",
704
+ limit: int = 50,
705
+ ) -> str:
706
+ """Get pull requests matching query parameters.
707
+
708
+ Args:
709
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
710
+ state (str, optional): State of the PRs to retrieve. Can be 'open', 'closed', or 'all'. Defaults to 'open'.
711
+ sort (str, optional): What to sort results by. Can be 'created', 'updated', 'popularity', 'long-running'. Defaults to 'created'.
712
+ direction (str, optional): The direction of the sort. Can be 'asc' or 'desc'. Defaults to 'desc'.
713
+ limit (int, optional): The maximum number of pull requests to return. Defaults to 20.
714
+
715
+ Returns:
716
+ A JSON-formatted string containing a list of pull requests.
717
+ """
718
+ try:
719
+ repo = self.g.get_repo(repo_name)
720
+ pulls = repo.get_pulls(state=state, sort=sort, direction=direction)
721
+
722
+ pr_list = []
723
+ for pr in pulls[:limit]:
724
+ pr_info = {
725
+ "number": pr.number,
726
+ "title": pr.title,
727
+ "user": pr.user.login,
728
+ "created_at": pr.created_at.isoformat(),
729
+ "updated_at": pr.updated_at.isoformat(),
730
+ "state": pr.state,
731
+ "url": pr.html_url,
732
+ }
733
+ pr_list.append(pr_info)
734
+
735
+ return json.dumps(pr_list, indent=2)
736
+ except GithubException as e:
737
+ logger.error(f"Error getting pull requests by query: {e}")
738
+ return json.dumps({"error": str(e)})
739
+
740
+ def get_pull_request_comments(self, repo_name: str, pr_number: int, include_issue_comments: bool = True) -> str:
741
+ """Get all comments on a pull request.
742
+
743
+ Args:
744
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
745
+ pr_number (int): The number of the pull request.
746
+ include_issue_comments (bool, optional): Whether to include general PR comments. Defaults to True.
747
+
748
+ Returns:
749
+ A JSON-formatted string containing a list of pull request comments.
750
+ """
751
+ log_debug(f"Getting comments for pull request #{pr_number} in repository: {repo_name}")
752
+ try:
753
+ repo = self.g.get_repo(repo_name)
754
+ pr = repo.get_pull(pr_number)
755
+
756
+ comment_list = []
757
+
758
+ # Get review comments (comments on specific lines of code)
759
+ review_comments = pr.get_comments()
760
+ for comment in review_comments:
761
+ comment_info = {
762
+ "id": comment.id,
763
+ "body": comment.body,
764
+ "user": comment.user.login,
765
+ "created_at": comment.created_at.isoformat(),
766
+ "updated_at": comment.updated_at.isoformat(),
767
+ "path": comment.path,
768
+ "position": comment.position,
769
+ "commit_id": comment.commit_id,
770
+ "url": comment.html_url,
771
+ "type": "review_comment",
772
+ }
773
+ comment_list.append(comment_info)
774
+
775
+ # Get general issue comments if requested
776
+ if include_issue_comments:
777
+ issue_comments = pr.get_issue_comments()
778
+ for comment in issue_comments:
779
+ comment_info = {
780
+ "id": comment.id,
781
+ "body": comment.body,
782
+ "user": comment.user.login,
783
+ "created_at": comment.created_at.isoformat(),
784
+ "updated_at": comment.updated_at.isoformat(),
785
+ "url": comment.html_url,
786
+ "type": "issue_comment",
787
+ }
788
+ comment_list.append(comment_info)
789
+
790
+ # Sort all comments by creation date
791
+ comment_list.sort(key=lambda x: x["created_at"], reverse=True)
792
+
793
+ return json.dumps(comment_list, indent=2)
794
+ except GithubException as e:
795
+ logger.error(f"Error getting pull request comments: {e}")
796
+ return json.dumps({"error": str(e)})
797
+
798
+ def create_pull_request_comment(
799
+ self,
800
+ repo_name: str,
801
+ pr_number: int,
802
+ body: str,
803
+ commit_id: str,
804
+ path: str,
805
+ position: int,
806
+ ) -> str:
807
+ """Create a comment on a specific line of a specific file in a pull request.
808
+
809
+ Args:
810
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
811
+ pr_number (int): The number of the pull request.
812
+ body (str): The text of the comment.
813
+ commit_id (str): The SHA of the commit to comment on.
814
+ path (str): The relative path to the file to comment on.
815
+ position (int): The line index in the diff to comment on.
816
+
817
+ Returns:
818
+ A JSON-formatted string containing the created comment details.
819
+ """
820
+ log_debug(f"Creating comment on pull request #{pr_number} in repository: {repo_name}")
821
+ try:
822
+ repo = self.g.get_repo(repo_name)
823
+ pr = repo.get_pull(pr_number)
824
+ commit = repo.get_commit(commit_id)
825
+ comment = pr.create_comment(body, commit, path, position)
826
+
827
+ comment_info = {
828
+ "id": comment.id,
829
+ "body": comment.body,
830
+ "user": comment.user.login,
831
+ "created_at": comment.created_at.isoformat(),
832
+ "path": comment.path,
833
+ "position": comment.position,
834
+ "commit_id": comment.commit_id,
835
+ "url": comment.html_url,
836
+ }
837
+
838
+ return json.dumps(comment_info, indent=2)
839
+ except GithubException as e:
840
+ logger.error(f"Error creating pull request comment: {e}")
841
+ return json.dumps({"error": str(e)})
842
+
843
+ def edit_pull_request_comment(self, repo_name: str, comment_id: int, body: str) -> str:
844
+ """Edit an existing pull request comment.
845
+
846
+ Args:
847
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
848
+ comment_id (int): The id of the comment to edit.
849
+ body (str): The new text of the comment.
850
+
851
+ Returns:
852
+ A JSON-formatted string containing the updated comment details.
853
+ """
854
+ log_debug(f"Editing comment #{comment_id} in repository: {repo_name}")
855
+ try:
856
+ repo = self.g.get_repo(repo_name)
857
+ comments = repo.get_pulls_comments()
858
+ comment = None
859
+ for comment in comments:
860
+ if comment.id == comment_id:
861
+ comment.edit(body)
862
+
863
+ if not comment:
864
+ return f"Could not find comment #{comment_id} in repository: {repo_name}"
865
+
866
+ comment_info = {
867
+ "id": comment.id,
868
+ "body": comment.body,
869
+ "user": comment.user.login,
870
+ "updated_at": comment.updated_at.isoformat(),
871
+ "path": comment.path,
872
+ "position": comment.position,
873
+ "commit_id": comment.commit_id,
874
+ "url": comment.html_url,
875
+ }
876
+
877
+ return json.dumps(comment_info, indent=2)
878
+ except GithubException as e:
879
+ logger.error(f"Error editing pull request comment: {e}")
880
+ return json.dumps({"error": str(e)})
881
+
882
+ def get_pull_request_with_details(self, repo_name: str, pr_number: int) -> str:
883
+ """Get comprehensive details of a pull request including comments, labels, and metadata.
884
+
885
+ Args:
886
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
887
+ pr_number (int): The number of the pull request.
888
+
889
+ Returns:
890
+ A JSON-formatted string containing detailed pull request information.
891
+ """
892
+ log_debug(f"Getting comprehensive details for PR #{pr_number} in repository: {repo_name}")
893
+ try:
894
+ repo = self.g.get_repo(repo_name)
895
+ pr = repo.get_pull(pr_number)
896
+
897
+ # Get review comments
898
+ review_comments = []
899
+ for comment in pr.get_comments():
900
+ review_comments.append(
901
+ {
902
+ "id": comment.id,
903
+ "body": comment.body,
904
+ "user": comment.user.login,
905
+ "created_at": comment.created_at.isoformat(),
906
+ "path": comment.path,
907
+ "position": comment.position,
908
+ "commit_id": comment.commit_id,
909
+ "url": comment.html_url,
910
+ "type": "review_comment",
911
+ }
912
+ )
913
+
914
+ # Get issue comments
915
+ issue_comments = []
916
+ for comment in pr.get_issue_comments():
917
+ issue_comments.append(
918
+ {
919
+ "id": comment.id,
920
+ "body": comment.body,
921
+ "user": comment.user.login,
922
+ "created_at": comment.created_at.isoformat(),
923
+ "url": comment.html_url,
924
+ "type": "issue_comment",
925
+ }
926
+ )
927
+
928
+ # Get commit data
929
+ commits = []
930
+ for commit in pr.get_commits():
931
+ commit_info = {
932
+ "sha": commit.sha,
933
+ "message": commit.commit.message,
934
+ "author": (commit.commit.author.name if commit.commit.author else "Unknown"),
935
+ "date": (commit.commit.author.date.isoformat() if commit.commit.author else None),
936
+ "url": commit.html_url,
937
+ }
938
+ commits.append(commit_info)
939
+
940
+ # Get files changed
941
+ files_changed = []
942
+ for file in pr.get_files():
943
+ file_info = {
944
+ "filename": file.filename,
945
+ "status": file.status,
946
+ "additions": file.additions,
947
+ "deletions": file.deletions,
948
+ "changes": file.changes,
949
+ "patch": file.patch,
950
+ }
951
+ files_changed.append(file_info)
952
+
953
+ # Combine all comments and sort by creation date
954
+ all_comments = review_comments + issue_comments
955
+ all_comments.sort(key=lambda x: x["created_at"], reverse=True)
956
+
957
+ # Get basic PR info
958
+ pr_info = {
959
+ "number": pr.number,
960
+ "title": pr.title,
961
+ "user": pr.user.login,
962
+ "state": pr.state,
963
+ "created_at": pr.created_at.isoformat(),
964
+ "updated_at": pr.updated_at.isoformat(),
965
+ "html_url": pr.html_url,
966
+ "body": pr.body,
967
+ "base": pr.base.ref,
968
+ "head": pr.head.ref,
969
+ "merged": pr.is_merged(),
970
+ "mergeable": pr.mergeable,
971
+ "additions": pr.additions,
972
+ "deletions": pr.deletions,
973
+ "changed_files": pr.changed_files,
974
+ "labels": [label.name for label in pr.labels],
975
+ "comments_count": {
976
+ "review_comments": len(review_comments),
977
+ "issue_comments": len(issue_comments),
978
+ "total": len(all_comments),
979
+ },
980
+ "comments": all_comments,
981
+ "commits": commits,
982
+ "files_changed": files_changed,
983
+ }
984
+
985
+ return json.dumps(pr_info, indent=2)
986
+ except GithubException as e:
987
+ logger.error(f"Error getting pull request details: {e}")
988
+ return json.dumps({"error": str(e)})
989
+
990
+ def get_repository_with_stats(self, repo_name: str) -> str:
991
+ """Get comprehensive repository information including statistics.
992
+
993
+ Args:
994
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
995
+
996
+ Returns:
997
+ A JSON-formatted string containing detailed repository information and statistics.
998
+ """
999
+ log_debug(f"Getting detailed info for repository: {repo_name}")
1000
+ try:
1001
+ repo = self.g.get_repo(repo_name)
1002
+
1003
+ # Helper function to safely convert values to primitive types
1004
+ def safe_value(val):
1005
+ if hasattr(val, "isoformat"):
1006
+ return val.isoformat()
1007
+ elif isinstance(val, (int, float, bool, str)) or val is None:
1008
+ return val
1009
+ else:
1010
+ return str(val)
1011
+
1012
+ # Get basic repo info
1013
+ repo_info = {
1014
+ "id": int(repo.id),
1015
+ "name": str(repo.name),
1016
+ "full_name": str(repo.full_name),
1017
+ "owner": str(repo.owner.login),
1018
+ "description": str(repo.description) if repo.description else None,
1019
+ "html_url": str(repo.html_url),
1020
+ "homepage": str(repo.homepage) if repo.homepage else None,
1021
+ "language": str(repo.language) if repo.language else None,
1022
+ "created_at": safe_value(repo.created_at),
1023
+ "updated_at": safe_value(repo.updated_at),
1024
+ "pushed_at": safe_value(repo.pushed_at),
1025
+ "size": int(repo.size),
1026
+ "stargazers_count": int(repo.stargazers_count),
1027
+ "watchers_count": int(repo.watchers_count),
1028
+ "forks_count": int(repo.forks_count),
1029
+ "open_issues_count": int(repo.open_issues_count),
1030
+ "default_branch": str(repo.default_branch),
1031
+ "topics": [str(topic) for topic in repo.get_topics()],
1032
+ "license": (str(repo.license.name) if repo.license and hasattr(repo.license, "name") else None),
1033
+ "private": bool(repo.private),
1034
+ "archived": bool(repo.archived),
1035
+ }
1036
+
1037
+ # Get languages
1038
+ repo_info["languages"] = {str(lang): int(count) for lang, count in repo.get_languages().items()}
1039
+
1040
+ # Calculate actual open issues (GitHub's count includes PRs)
1041
+ try:
1042
+ open_issues_count = 0
1043
+ for issue in repo.get_issues(state="open"):
1044
+ if not issue.pull_request:
1045
+ open_issues_count += 1
1046
+ repo_info["actual_open_issues"] = open_issues_count
1047
+ except Exception as e:
1048
+ log_debug(f"Error getting actual open issues: {e}")
1049
+ repo_info["actual_open_issues"] = None
1050
+
1051
+ # Get open pull requests count
1052
+ try:
1053
+ open_prs = repo.get_pulls(state="open")
1054
+ repo_info["open_pr_count"] = int(open_prs.totalCount)
1055
+ except Exception as e:
1056
+ log_debug(f"Error getting open PRs count: {e}")
1057
+ repo_info["open_pr_count"] = None
1058
+
1059
+ # Get recent open PRs
1060
+ try:
1061
+ open_prs_list = []
1062
+ open_prs = repo.get_pulls(state="open")
1063
+
1064
+ # Use a simple for loop approach instead of trying to slice first
1065
+ count = 0
1066
+ for pr in open_prs:
1067
+ if count >= 10:
1068
+ break
1069
+ try:
1070
+ # Ensure all fields are primitives, not Mock objects
1071
+ pr_data = {
1072
+ "number": int(pr.number),
1073
+ "title": str(pr.title),
1074
+ "user": str(pr.user.login),
1075
+ "created_at": safe_value(pr.created_at),
1076
+ "updated_at": safe_value(pr.updated_at),
1077
+ "url": str(pr.html_url),
1078
+ "base": str(pr.base.ref),
1079
+ "head": str(pr.head.ref),
1080
+ "comment_count": int(pr.comments),
1081
+ }
1082
+ open_prs_list.append(pr_data)
1083
+ count += 1
1084
+ except Exception as e:
1085
+ log_debug(f"Error processing individual PR: {e}")
1086
+
1087
+ repo_info["recent_open_prs"] = open_prs_list
1088
+ except Exception as e:
1089
+ log_debug(f"Error getting recent open PRs: {e}")
1090
+ repo_info["recent_open_prs"] = []
1091
+
1092
+ # Calculate PR metrics
1093
+ try:
1094
+ # Get a sample of PRs for statistics
1095
+ all_prs_list = []
1096
+ all_prs = repo.get_pulls(state="all", sort="created", direction="desc")
1097
+
1098
+ pr_count = 0
1099
+ for pr in all_prs:
1100
+ if pr_count >= 100: # Limit to 100 PRs
1101
+ break
1102
+ all_prs_list.append(pr)
1103
+ pr_count += 1
1104
+
1105
+ # Calculate basic metrics
1106
+ merged_prs = []
1107
+ for pr in all_prs_list:
1108
+ is_merged = pr.is_merged()
1109
+ if is_merged:
1110
+ merged_prs.append(pr)
1111
+
1112
+ # Compute merge time for merged PRs (in hours)
1113
+ merge_times = []
1114
+ for pr in merged_prs:
1115
+ if pr.merged_at and pr.created_at:
1116
+ merge_time = (pr.merged_at - pr.created_at).total_seconds() / 3600
1117
+ merge_times.append(merge_time)
1118
+
1119
+ pr_metrics = {
1120
+ "total_prs": len(all_prs_list),
1121
+ "merged_prs": len(merged_prs),
1122
+ "acceptance_rate": ((len(merged_prs) / len(all_prs_list) * 100) if len(all_prs_list) > 0 else 0),
1123
+ "avg_time_to_merge": (sum(merge_times) / len(merge_times) if merge_times else None),
1124
+ }
1125
+ repo_info["pr_metrics"] = pr_metrics
1126
+ except Exception as e:
1127
+ log_debug(f"Error calculating PR metrics: {e}")
1128
+ repo_info["pr_metrics"] = None
1129
+
1130
+ # Get contributors
1131
+ try:
1132
+ contributors: list[dict] = []
1133
+ for contributor in repo.get_contributors():
1134
+ if len(contributors) >= 20: # Limit to top 20
1135
+ break
1136
+ contributors.append(
1137
+ {
1138
+ "login": str(contributor.login),
1139
+ "contributions": int(contributor.contributions),
1140
+ "url": str(contributor.html_url),
1141
+ }
1142
+ )
1143
+ repo_info["contributors"] = contributors
1144
+ except Exception as e:
1145
+ log_debug(f"Error getting contributors: {e}")
1146
+ repo_info["contributors"] = []
1147
+
1148
+ return json.dumps(repo_info, indent=2)
1149
+ except GithubException as e:
1150
+ logger.error(f"Error getting repository stats: {e}")
1151
+ return json.dumps({"error": str(e)})
1152
+
1153
+ def create_pull_request(
1154
+ self,
1155
+ repo_name: str,
1156
+ title: str,
1157
+ body: str,
1158
+ head: str,
1159
+ base: str,
1160
+ draft: bool = False,
1161
+ maintainer_can_modify: bool = True,
1162
+ ) -> str:
1163
+ """Create a new pull request in a repository.
1164
+
1165
+ Args:
1166
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1167
+ title (str): The title of the pull request.
1168
+ body (str): The body text of the pull request.
1169
+ head (str): The name of the branch where your changes are implemented.
1170
+ base (str): The name of the branch you want the changes pulled into.
1171
+ draft (bool, optional): Whether the pull request is a draft. Defaults to False.
1172
+ maintainer_can_modify (bool, optional): Whether maintainers can modify the PR. Defaults to True.
1173
+
1174
+ Returns:
1175
+ A JSON-formatted string containing the created pull request details.
1176
+ """
1177
+ log_debug(f"Creating pull request in repository: {repo_name}")
1178
+ try:
1179
+ repo = self.g.get_repo(repo_name)
1180
+ pr = repo.create_pull(
1181
+ title=title,
1182
+ body=body,
1183
+ head=head,
1184
+ base=base,
1185
+ draft=draft,
1186
+ maintainer_can_modify=maintainer_can_modify,
1187
+ )
1188
+
1189
+ pr_info = {
1190
+ "number": pr.number,
1191
+ "title": pr.title,
1192
+ "body": pr.body,
1193
+ "user": pr.user.login,
1194
+ "state": pr.state,
1195
+ "created_at": pr.created_at.isoformat(),
1196
+ "html_url": pr.html_url,
1197
+ "base": pr.base.ref,
1198
+ "head": pr.head.ref,
1199
+ "mergeable": pr.mergeable,
1200
+ }
1201
+
1202
+ return json.dumps(pr_info, indent=2)
1203
+ except GithubException as e:
1204
+ logger.error(f"Error creating pull request: {e}")
1205
+ return json.dumps({"error": str(e)})
1206
+
1207
+ def create_review_request(
1208
+ self,
1209
+ repo_name: str,
1210
+ pr_number: int,
1211
+ reviewers: List[str],
1212
+ team_reviewers: Optional[List[str]] = None,
1213
+ ) -> str:
1214
+ """Create a review request for a pull request.
1215
+
1216
+ Args:
1217
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1218
+ pr_number (int): The number of the pull request.
1219
+ reviewers (List[str]): List of user logins that will be requested to review.
1220
+ team_reviewers (List[str], optional): List of team slugs that will be requested to review. Defaults to None.
1221
+
1222
+ Returns:
1223
+ A JSON-formatted string with the success message or error.
1224
+ """
1225
+ log_debug(f"Creating review request for PR #{pr_number} in repository: {repo_name}")
1226
+ try:
1227
+ repo = self.g.get_repo(repo_name)
1228
+ pr = repo.get_pull(pr_number)
1229
+ pr.create_review_request(reviewers=reviewers, team_reviewers=team_reviewers or [])
1230
+
1231
+ return json.dumps(
1232
+ {
1233
+ "message": f"Review request created for PR #{pr_number}",
1234
+ "requested_reviewers": reviewers,
1235
+ "requested_team_reviewers": team_reviewers or [],
1236
+ },
1237
+ indent=2,
1238
+ )
1239
+ except GithubException as e:
1240
+ logger.error(f"Error creating review request: {e}")
1241
+ return json.dumps({"error": str(e)})
1242
+
1243
+ def create_file(
1244
+ self,
1245
+ repo_name: str,
1246
+ path: str,
1247
+ content: str,
1248
+ message: str,
1249
+ branch: Optional[str] = None,
1250
+ ) -> str:
1251
+ """Create a new file in a repository.
1252
+
1253
+ Args:
1254
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1255
+ path (str): The path to the file in the repository.
1256
+ content (str): The content of the file.
1257
+ message (str): The commit message.
1258
+ branch (str, optional): The branch to commit to. Defaults to repository's default branch.
1259
+
1260
+ Returns:
1261
+ A JSON-formatted string containing the file creation result.
1262
+ """
1263
+ log_debug(f"Creating file {path} in repository: {repo_name}")
1264
+ try:
1265
+ repo = self.g.get_repo(repo_name)
1266
+
1267
+ # Convert string content to bytes
1268
+ content_bytes = content.encode("utf-8")
1269
+
1270
+ # Create the file
1271
+ result = repo.create_file(path=path, message=message, content=content_bytes, branch=branch)
1272
+
1273
+ # Extract relevant information
1274
+ file_info = {
1275
+ "path": result["content"].path, # type: ignore
1276
+ "sha": result["content"].sha,
1277
+ "url": result["content"].html_url,
1278
+ "commit": {
1279
+ "sha": result["commit"].sha,
1280
+ "message": result["commit"].commit.message
1281
+ if result["commit"].commit
1282
+ else result["commit"]._rawData["message"],
1283
+ "url": result["commit"].html_url,
1284
+ },
1285
+ }
1286
+
1287
+ return json.dumps(file_info, indent=2)
1288
+ except (GithubException, AssertionError) as e:
1289
+ logger.error(f"Error creating file: {e}")
1290
+ return json.dumps({"error": str(e)})
1291
+
1292
+ def get_file_content(self, repo_name: str, path: str, ref: Optional[str] = None) -> str:
1293
+ """Get the content of a file in a repository.
1294
+
1295
+ Args:
1296
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1297
+ path (str): The path to the file in the repository.
1298
+ ref (str, optional): The name of the commit/branch/tag. Defaults to the repository's default branch.
1299
+
1300
+ Returns:
1301
+ A JSON-formatted string containing the file content and metadata.
1302
+ """
1303
+ log_debug(f"Getting content of file {path} in repository: {repo_name}")
1304
+ try:
1305
+ repo = self.g.get_repo(repo_name)
1306
+
1307
+ # Conditionally call get_contents based on ref
1308
+ if ref is not None:
1309
+ file_content = repo.get_contents(path, ref=ref)
1310
+ else:
1311
+ file_content = repo.get_contents(path)
1312
+
1313
+ # If it's a list (directory), raise an error
1314
+ if isinstance(file_content, list):
1315
+ return json.dumps({"error": f"{path} is a directory, not a file"})
1316
+
1317
+ # Decode content
1318
+ try:
1319
+ decoded_content = file_content.decoded_content.decode("utf-8")
1320
+ except UnicodeDecodeError:
1321
+ decoded_content = "Binary file (content not displayed)"
1322
+ except Exception as e:
1323
+ log_debug(f"Error decoding file content: {e}")
1324
+ decoded_content = "Binary file (content not displayed)"
1325
+
1326
+ # Make sure we don't try to display binary content
1327
+ if isinstance(decoded_content, str) and (
1328
+ "\x00" in decoded_content or sum(1 for c in decoded_content[:1000] if not (32 <= ord(c) <= 126)) > 200
1329
+ ):
1330
+ decoded_content = "Binary file (content not displayed)"
1331
+
1332
+ # Create response
1333
+ content_info = {
1334
+ "name": file_content.name,
1335
+ "path": file_content.path,
1336
+ "sha": file_content.sha,
1337
+ "size": file_content.size,
1338
+ "type": file_content.type,
1339
+ "url": file_content.html_url,
1340
+ "content": decoded_content,
1341
+ }
1342
+
1343
+ return json.dumps(content_info, indent=2)
1344
+ except GithubException as e:
1345
+ logger.error(f"Error getting file content: {e}")
1346
+ return json.dumps({"error": str(e)})
1347
+
1348
+ def update_file(
1349
+ self,
1350
+ repo_name: str,
1351
+ path: str,
1352
+ content: str,
1353
+ message: str,
1354
+ sha: str,
1355
+ branch: Optional[str] = None,
1356
+ ) -> str:
1357
+ """Update an existing file in a repository.
1358
+
1359
+ Args:
1360
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1361
+ path (str): The path to the file in the repository.
1362
+ content (str): The new content of the file.
1363
+ message (str): The commit message.
1364
+ sha (str): The blob SHA of the file being replaced.
1365
+ branch (str, optional): The branch to commit to. Defaults to repository's default branch.
1366
+
1367
+ Returns:
1368
+ A JSON-formatted string containing the file update result.
1369
+ """
1370
+ log_debug(f"Updating file {path} in repository: {repo_name}")
1371
+ try:
1372
+ repo = self.g.get_repo(repo_name)
1373
+
1374
+ # Convert string content to bytes
1375
+ content_bytes = content.encode("utf-8")
1376
+
1377
+ # Update the file
1378
+ result = repo.update_file(
1379
+ path=path,
1380
+ message=message,
1381
+ content=content_bytes,
1382
+ sha=sha,
1383
+ branch=branch,
1384
+ )
1385
+
1386
+ # Extract relevant information
1387
+ file_info = {
1388
+ "path": result["content"].path,
1389
+ "sha": result["content"].sha,
1390
+ "url": result["content"].html_url,
1391
+ "commit": {
1392
+ "sha": result["commit"].sha,
1393
+ "message": result["commit"].commit.message,
1394
+ "url": result["commit"].html_url,
1395
+ },
1396
+ }
1397
+
1398
+ return json.dumps(file_info, indent=2)
1399
+ except GithubException as e:
1400
+ logger.error(f"Error updating file: {e}")
1401
+ return json.dumps({"error": str(e)})
1402
+
1403
+ def delete_file(
1404
+ self,
1405
+ repo_name: str,
1406
+ path: str,
1407
+ message: str,
1408
+ sha: str,
1409
+ branch: Optional[str] = None,
1410
+ ) -> str:
1411
+ """Delete a file from a repository.
1412
+
1413
+ Args:
1414
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1415
+ path (str): The path to the file in the repository.
1416
+ message (str): The commit message.
1417
+ sha (str): The blob SHA of the file being deleted.
1418
+ branch (str, optional): The branch to commit to. Defaults to repository's default branch.
1419
+
1420
+ Returns:
1421
+ A JSON-formatted string containing the file deletion result.
1422
+ """
1423
+ log_debug(f"Deleting file {path} in repository: {repo_name}")
1424
+ try:
1425
+ repo = self.g.get_repo(repo_name)
1426
+
1427
+ # Delete the file
1428
+ result = repo.delete_file(path=path, message=message, sha=sha, branch=branch)
1429
+
1430
+ # Extract relevant information
1431
+ commit_info = {
1432
+ "message": f"File {path} deleted successfully",
1433
+ "commit": {
1434
+ "sha": result["commit"].sha,
1435
+ "message": result["commit"].commit.message,
1436
+ "url": result["commit"].html_url,
1437
+ },
1438
+ }
1439
+
1440
+ return json.dumps(commit_info, indent=2)
1441
+ except GithubException as e:
1442
+ logger.error(f"Error deleting file: {e}")
1443
+ return json.dumps({"error": str(e)})
1444
+
1445
+ def get_directory_content(self, repo_name: str, path: str, ref: Optional[str] = None) -> str:
1446
+ """Get the contents of a directory in a repository.
1447
+
1448
+ Args:
1449
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1450
+ path (str): The path to the directory in the repository. Use empty string for root.
1451
+ ref (str, optional): The name of the commit/branch/tag. Defaults to repository's default branch.
1452
+
1453
+ Returns:
1454
+ A JSON-formatted string containing a list of directory contents.
1455
+ """
1456
+ log_debug(f"Getting contents of directory {path} in repository: {repo_name}")
1457
+ try:
1458
+ repo = self.g.get_repo(repo_name)
1459
+
1460
+ # Conditionally call get_contents based on ref
1461
+ if ref is not None:
1462
+ contents = repo.get_contents(path, ref=ref)
1463
+ else:
1464
+ contents = repo.get_contents(path)
1465
+
1466
+ # If it's not a list, it's a file not a directory
1467
+ if not isinstance(contents, list):
1468
+ return json.dumps({"error": f"{path} is a file, not a directory"})
1469
+
1470
+ # Process directory contents
1471
+ items = []
1472
+ for content in contents:
1473
+ item = {
1474
+ "name": content.name,
1475
+ "path": content.path,
1476
+ "type": content.type,
1477
+ "size": content.size,
1478
+ "sha": content.sha,
1479
+ "url": content.html_url,
1480
+ "download_url": content.download_url,
1481
+ }
1482
+ items.append(item)
1483
+
1484
+ # Sort by type (directories first) and then by name
1485
+ items.sort(key=lambda x: (x["type"] != "dir", x["name"].lower()))
1486
+
1487
+ return json.dumps(items, indent=2)
1488
+ except GithubException as e:
1489
+ logger.error(f"Error getting directory contents: {e}")
1490
+ return json.dumps({"error": str(e)})
1491
+
1492
+ def get_branch_content(self, repo_name: str, branch: str = "main") -> str:
1493
+ """Get the root directory content of a specific branch.
1494
+
1495
+ Args:
1496
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1497
+ branch (str, optional): The branch name. Defaults to "main".
1498
+
1499
+ Returns:
1500
+ A JSON-formatted string containing a list of branch contents.
1501
+ """
1502
+ log_debug(f"Getting contents of branch {branch} in repository: {repo_name}")
1503
+ try:
1504
+ # This is just a convenience function that uses get_directory_content with empty path
1505
+ return self.get_directory_content(repo_name=repo_name, path="", ref=branch)
1506
+ except GithubException as e:
1507
+ logger.error(f"Error getting branch contents: {e}")
1508
+ return json.dumps({"error": str(e)})
1509
+
1510
+ def create_branch(self, repo_name: str, branch_name: str, source_branch: Optional[str] = None) -> str:
1511
+ """Create a new branch in a repository.
1512
+
1513
+ Args:
1514
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1515
+ branch_name (str): The name of the new branch.
1516
+ source_branch (str, optional): The source branch to create from. Defaults to repository's default branch.
1517
+
1518
+ Returns:
1519
+ A JSON-formatted string containing information about the created branch.
1520
+ """
1521
+ log_debug(f"Creating branch {branch_name} in repository: {repo_name}")
1522
+ try:
1523
+ repo = self.g.get_repo(repo_name)
1524
+
1525
+ # Get the source branch or default branch if not specified
1526
+ if source_branch is None:
1527
+ source_branch = repo.default_branch
1528
+
1529
+ # Get the SHA of the latest commit on the source branch
1530
+ source_branch_ref = repo.get_git_ref(f"heads/{source_branch}")
1531
+ sha = source_branch_ref.object.sha
1532
+
1533
+ # Create the new branch
1534
+ new_branch = repo.create_git_ref(f"refs/heads/{branch_name}", sha)
1535
+
1536
+ branch_info = {
1537
+ "name": branch_name,
1538
+ "sha": new_branch.object.sha,
1539
+ "url": new_branch.url.replace("api.github.com/repos", "github.com").replace("git/refs/heads", "tree"),
1540
+ }
1541
+
1542
+ return json.dumps(branch_info, indent=2)
1543
+ except GithubException as e:
1544
+ logger.error(f"Error creating branch: {e}")
1545
+ return json.dumps({"error": str(e)})
1546
+
1547
+ def set_default_branch(self, repo_name: str, branch_name: str) -> str:
1548
+ """Set the default branch for a repository.
1549
+
1550
+ Args:
1551
+ repo_name (str): The full name of the repository (e.g., 'owner/repo').
1552
+ branch_name (str): The name of the branch to set as default.
1553
+
1554
+ Returns:
1555
+ A JSON-formatted string with success message or error.
1556
+ """
1557
+ log_debug(f"Setting default branch to {branch_name} in repository: {repo_name}")
1558
+ try:
1559
+ repo = self.g.get_repo(repo_name)
1560
+
1561
+ # Check if the branch exists by looking at all branches
1562
+ branches = [branch.name for branch in repo.get_branches()]
1563
+ if branch_name not in branches:
1564
+ return json.dumps({"error": f"Branch '{branch_name}' does not exist"})
1565
+
1566
+ # Set the default branch
1567
+ repo.edit(default_branch=branch_name)
1568
+
1569
+ return json.dumps(
1570
+ {
1571
+ "message": f"Default branch changed to {branch_name}",
1572
+ "repository": repo_name,
1573
+ "default_branch": branch_name,
1574
+ },
1575
+ indent=2,
1576
+ )
1577
+ except GithubException as e:
1578
+ logger.error(f"Error setting default branch: {e}")
1579
+ return json.dumps({"error": str(e)})
1580
+
1581
+ def search_code(
1582
+ self,
1583
+ query: str,
1584
+ language: Optional[str] = None,
1585
+ repo: Optional[str] = None,
1586
+ user: Optional[str] = None,
1587
+ path: Optional[str] = None,
1588
+ filename: Optional[str] = None,
1589
+ ) -> str:
1590
+ """Search for code in GitHub repositories.
1591
+
1592
+ Args:
1593
+ query (str): The search query.
1594
+ language (str, optional): Filter by language. Defaults to None.
1595
+ repo (str, optional): Filter by repository (e.g., 'owner/repo'). Defaults to None.
1596
+ user (str, optional): Filter by user or organization. Defaults to None.
1597
+ path (str, optional): Filter by file path. Defaults to None.
1598
+ filename (str, optional): Filter by filename. Defaults to None.
1599
+
1600
+ Returns:
1601
+ A JSON-formatted string containing the search results.
1602
+ """
1603
+ log_debug(f"Searching code with query: {query}")
1604
+ try:
1605
+ search_query = query
1606
+
1607
+ # Add filters to the query if provided
1608
+ if language:
1609
+ search_query += f" language:{language}"
1610
+ if repo:
1611
+ search_query += f" repo:{repo}"
1612
+ if user:
1613
+ search_query += f" user:{user}"
1614
+ if path:
1615
+ search_query += f" path:{path}"
1616
+ if filename:
1617
+ search_query += f" filename:{filename}"
1618
+
1619
+ # Perform the search
1620
+ log_debug(f"Final search query: {search_query}")
1621
+ code_results = self.g.search_code(search_query)
1622
+
1623
+ results: list[dict] = []
1624
+ limit = 60
1625
+ max_pages = 2 # GitHub returns 30 items per page, so 2 pages covers our limit
1626
+ page_index = 0
1627
+
1628
+ while len(results) < limit and page_index < max_pages:
1629
+ # Fetch one page of results from GitHub API
1630
+ page_items = code_results.get_page(page_index)
1631
+
1632
+ # Stop if no more results available
1633
+ if not page_items:
1634
+ break
1635
+
1636
+ # Process each code result in the current page
1637
+ for code in page_items:
1638
+ code_info = {
1639
+ "repository": code.repository.full_name,
1640
+ "path": code.path,
1641
+ "name": code.name,
1642
+ "sha": code.sha,
1643
+ "html_url": code.html_url,
1644
+ "git_url": code.git_url,
1645
+ "score": code.score,
1646
+ }
1647
+ results.append(code_info)
1648
+ page_index += 1
1649
+
1650
+ # Return search results
1651
+ return json.dumps(
1652
+ {
1653
+ "query": search_query,
1654
+ "total_count": code_results.totalCount,
1655
+ "results_count": len(results),
1656
+ "results": results,
1657
+ },
1658
+ indent=2,
1659
+ )
1660
+ except GithubException as e:
1661
+ logger.error(f"Error searching code: {e}")
1662
+ return json.dumps({"error": str(e)})
1663
+
1664
+ def search_issues_and_prs(
1665
+ self,
1666
+ query: str,
1667
+ state: Optional[str] = None,
1668
+ type_filter: Optional[str] = None,
1669
+ repo: Optional[str] = None,
1670
+ user: Optional[str] = None,
1671
+ label: Optional[str] = None,
1672
+ sort: str = "created",
1673
+ order: str = "desc",
1674
+ page: int = 1,
1675
+ per_page: int = 30,
1676
+ ) -> str:
1677
+ """Search for issues and pull requests on GitHub.
1678
+
1679
+ Args:
1680
+ query (str): The search query.
1681
+ state (str, optional): Filter by state ('open', 'closed'). Defaults to None.
1682
+ type_filter (str, optional): Filter by type ('issue', 'pr'). Defaults to None.
1683
+ repo (str, optional): Filter by repository (e.g., 'owner/repo'). Defaults to None.
1684
+ user (str, optional): Filter by user or organization. Defaults to None.
1685
+ label (str, optional): Filter by label. Defaults to None.
1686
+ sort (str, optional): Sort results by ('created', 'updated', 'comments'). Defaults to "created".
1687
+ order (str, optional): Sort order ('asc', 'desc'). Defaults to "desc".
1688
+ page (int, optional): Page number for pagination. Defaults to 1.
1689
+ per_page (int, optional): Number of results per page. Defaults to 30.
1690
+
1691
+ Returns:
1692
+ A JSON-formatted string containing the search results.
1693
+ """
1694
+ log_debug(f"Searching issues and PRs with query: {query}")
1695
+ try:
1696
+ search_query = query
1697
+
1698
+ # Add filters to the query if provided
1699
+ if state:
1700
+ search_query += f" state:{state}"
1701
+ if type_filter == "issue":
1702
+ search_query += " is:issue"
1703
+ elif type_filter == "pr":
1704
+ search_query += " is:pr"
1705
+ if repo:
1706
+ search_query += f" repo:{repo}"
1707
+ if user:
1708
+ search_query += f" user:{user}"
1709
+ if label:
1710
+ search_query += f" label:{label}"
1711
+
1712
+ # Perform the search
1713
+ log_debug(f"Final search query: {search_query}")
1714
+ issue_results = self.g.search_issues(search_query, sort=sort, order=order)
1715
+
1716
+ # Process results
1717
+ per_page = min(per_page, 100) # Ensure per_page doesn't exceed 100
1718
+ results = []
1719
+
1720
+ try:
1721
+ # Get the specific page of results
1722
+ page_items = issue_results.get_page(page - 1)
1723
+
1724
+ for issue in page_items:
1725
+ issue_info = {
1726
+ "number": issue.number,
1727
+ "title": issue.title,
1728
+ "repository": issue.repository.full_name,
1729
+ "state": issue.state,
1730
+ "created_at": issue.created_at.isoformat(),
1731
+ "updated_at": issue.updated_at.isoformat(),
1732
+ "html_url": issue.html_url,
1733
+ "user": issue.user.login,
1734
+ "is_pull_request": hasattr(issue, "pull_request") and issue.pull_request is not None,
1735
+ "comments": issue.comments,
1736
+ "labels": [label.name for label in issue.labels],
1737
+ }
1738
+ results.append(issue_info)
1739
+
1740
+ if len(results) >= per_page:
1741
+ break
1742
+ except IndexError:
1743
+ # Page is out of range
1744
+ pass
1745
+
1746
+ # Return search results
1747
+ return json.dumps(
1748
+ {
1749
+ "query": search_query,
1750
+ "total_count": issue_results.totalCount,
1751
+ "page": page,
1752
+ "per_page": per_page,
1753
+ "results_count": len(results),
1754
+ "results": results,
1755
+ },
1756
+ indent=2,
1757
+ )
1758
+ except GithubException as e:
1759
+ logger.error(f"Error searching issues and PRs: {e}")
1760
+ return json.dumps({"error": str(e)})