agno 0.1.2__py3-none-any.whl → 2.3.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 (723) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +44 -5
  3. agno/agent/agent.py +10531 -2975
  4. agno/api/agent.py +14 -53
  5. agno/api/api.py +7 -46
  6. agno/api/evals.py +22 -0
  7. agno/api/os.py +17 -0
  8. agno/api/routes.py +6 -25
  9. agno/api/schemas/__init__.py +9 -0
  10. agno/api/schemas/agent.py +6 -9
  11. agno/api/schemas/evals.py +16 -0
  12. agno/api/schemas/os.py +14 -0
  13. agno/api/schemas/team.py +10 -10
  14. agno/api/schemas/utils.py +21 -0
  15. agno/api/schemas/workflows.py +16 -0
  16. agno/api/settings.py +53 -0
  17. agno/api/team.py +22 -26
  18. agno/api/workflow.py +28 -0
  19. agno/cloud/aws/base.py +214 -0
  20. agno/cloud/aws/s3/__init__.py +2 -0
  21. agno/cloud/aws/s3/api_client.py +43 -0
  22. agno/cloud/aws/s3/bucket.py +195 -0
  23. agno/cloud/aws/s3/object.py +57 -0
  24. agno/compression/__init__.py +3 -0
  25. agno/compression/manager.py +247 -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 +946 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2781 -0
  33. agno/db/dynamo/schemas.py +442 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +2379 -0
  37. agno/db/firestore/schemas.py +181 -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 +1791 -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 +1312 -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 +1777 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/manager.py +199 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/migrations/versions/v2_3_0.py +938 -0
  51. agno/db/mongo/__init__.py +17 -0
  52. agno/db/mongo/async_mongo.py +2760 -0
  53. agno/db/mongo/mongo.py +2597 -0
  54. agno/db/mongo/schemas.py +119 -0
  55. agno/db/mongo/utils.py +276 -0
  56. agno/db/mysql/__init__.py +4 -0
  57. agno/db/mysql/async_mysql.py +2912 -0
  58. agno/db/mysql/mysql.py +2923 -0
  59. agno/db/mysql/schemas.py +186 -0
  60. agno/db/mysql/utils.py +488 -0
  61. agno/db/postgres/__init__.py +4 -0
  62. agno/db/postgres/async_postgres.py +2579 -0
  63. agno/db/postgres/postgres.py +2870 -0
  64. agno/db/postgres/schemas.py +187 -0
  65. agno/db/postgres/utils.py +442 -0
  66. agno/db/redis/__init__.py +3 -0
  67. agno/db/redis/redis.py +2141 -0
  68. agno/db/redis/schemas.py +159 -0
  69. agno/db/redis/utils.py +346 -0
  70. agno/db/schemas/__init__.py +4 -0
  71. agno/db/schemas/culture.py +120 -0
  72. agno/db/schemas/evals.py +34 -0
  73. agno/db/schemas/knowledge.py +40 -0
  74. agno/db/schemas/memory.py +61 -0
  75. agno/db/singlestore/__init__.py +3 -0
  76. agno/db/singlestore/schemas.py +179 -0
  77. agno/db/singlestore/singlestore.py +2877 -0
  78. agno/db/singlestore/utils.py +384 -0
  79. agno/db/sqlite/__init__.py +4 -0
  80. agno/db/sqlite/async_sqlite.py +2911 -0
  81. agno/db/sqlite/schemas.py +181 -0
  82. agno/db/sqlite/sqlite.py +2908 -0
  83. agno/db/sqlite/utils.py +429 -0
  84. agno/db/surrealdb/__init__.py +3 -0
  85. agno/db/surrealdb/metrics.py +292 -0
  86. agno/db/surrealdb/models.py +334 -0
  87. agno/db/surrealdb/queries.py +71 -0
  88. agno/db/surrealdb/surrealdb.py +1908 -0
  89. agno/db/surrealdb/utils.py +147 -0
  90. agno/db/utils.py +118 -0
  91. agno/eval/__init__.py +24 -0
  92. agno/eval/accuracy.py +666 -276
  93. agno/eval/agent_as_judge.py +861 -0
  94. agno/eval/base.py +29 -0
  95. agno/eval/performance.py +779 -0
  96. agno/eval/reliability.py +241 -62
  97. agno/eval/utils.py +120 -0
  98. agno/exceptions.py +143 -1
  99. agno/filters.py +354 -0
  100. agno/guardrails/__init__.py +6 -0
  101. agno/guardrails/base.py +19 -0
  102. agno/guardrails/openai.py +144 -0
  103. agno/guardrails/pii.py +94 -0
  104. agno/guardrails/prompt_injection.py +52 -0
  105. agno/hooks/__init__.py +3 -0
  106. agno/hooks/decorator.py +164 -0
  107. agno/integrations/discord/__init__.py +3 -0
  108. agno/integrations/discord/client.py +203 -0
  109. agno/knowledge/__init__.py +5 -1
  110. agno/{document → knowledge}/chunking/agentic.py +22 -14
  111. agno/{document → knowledge}/chunking/document.py +2 -2
  112. agno/{document → knowledge}/chunking/fixed.py +7 -6
  113. agno/knowledge/chunking/markdown.py +151 -0
  114. agno/{document → knowledge}/chunking/recursive.py +15 -3
  115. agno/knowledge/chunking/row.py +39 -0
  116. agno/knowledge/chunking/semantic.py +91 -0
  117. agno/knowledge/chunking/strategy.py +165 -0
  118. agno/knowledge/content.py +74 -0
  119. agno/knowledge/document/__init__.py +5 -0
  120. agno/{document → knowledge/document}/base.py +12 -2
  121. agno/knowledge/embedder/__init__.py +5 -0
  122. agno/knowledge/embedder/aws_bedrock.py +343 -0
  123. agno/knowledge/embedder/azure_openai.py +210 -0
  124. agno/{embedder → knowledge/embedder}/base.py +8 -0
  125. agno/knowledge/embedder/cohere.py +323 -0
  126. agno/knowledge/embedder/fastembed.py +62 -0
  127. agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
  128. agno/knowledge/embedder/google.py +258 -0
  129. agno/knowledge/embedder/huggingface.py +94 -0
  130. agno/knowledge/embedder/jina.py +182 -0
  131. agno/knowledge/embedder/langdb.py +22 -0
  132. agno/knowledge/embedder/mistral.py +206 -0
  133. agno/knowledge/embedder/nebius.py +13 -0
  134. agno/knowledge/embedder/ollama.py +154 -0
  135. agno/knowledge/embedder/openai.py +195 -0
  136. agno/knowledge/embedder/sentence_transformer.py +63 -0
  137. agno/{embedder → knowledge/embedder}/together.py +1 -1
  138. agno/knowledge/embedder/vllm.py +262 -0
  139. agno/knowledge/embedder/voyageai.py +165 -0
  140. agno/knowledge/knowledge.py +3006 -0
  141. agno/knowledge/reader/__init__.py +7 -0
  142. agno/knowledge/reader/arxiv_reader.py +81 -0
  143. agno/knowledge/reader/base.py +95 -0
  144. agno/knowledge/reader/csv_reader.py +164 -0
  145. agno/knowledge/reader/docx_reader.py +82 -0
  146. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  147. agno/knowledge/reader/firecrawl_reader.py +201 -0
  148. agno/knowledge/reader/json_reader.py +88 -0
  149. agno/knowledge/reader/markdown_reader.py +137 -0
  150. agno/knowledge/reader/pdf_reader.py +431 -0
  151. agno/knowledge/reader/pptx_reader.py +101 -0
  152. agno/knowledge/reader/reader_factory.py +313 -0
  153. agno/knowledge/reader/s3_reader.py +89 -0
  154. agno/knowledge/reader/tavily_reader.py +193 -0
  155. agno/knowledge/reader/text_reader.py +127 -0
  156. agno/knowledge/reader/web_search_reader.py +325 -0
  157. agno/knowledge/reader/website_reader.py +455 -0
  158. agno/knowledge/reader/wikipedia_reader.py +91 -0
  159. agno/knowledge/reader/youtube_reader.py +78 -0
  160. agno/knowledge/remote_content/remote_content.py +88 -0
  161. agno/knowledge/reranker/__init__.py +3 -0
  162. agno/{reranker → knowledge/reranker}/base.py +1 -1
  163. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  164. agno/knowledge/reranker/infinity.py +195 -0
  165. agno/knowledge/reranker/sentence_transformer.py +54 -0
  166. agno/knowledge/types.py +39 -0
  167. agno/knowledge/utils.py +234 -0
  168. agno/media.py +439 -95
  169. agno/memory/__init__.py +16 -3
  170. agno/memory/manager.py +1474 -123
  171. agno/memory/strategies/__init__.py +15 -0
  172. agno/memory/strategies/base.py +66 -0
  173. agno/memory/strategies/summarize.py +196 -0
  174. agno/memory/strategies/types.py +37 -0
  175. agno/models/aimlapi/__init__.py +5 -0
  176. agno/models/aimlapi/aimlapi.py +62 -0
  177. agno/models/anthropic/__init__.py +4 -0
  178. agno/models/anthropic/claude.py +960 -496
  179. agno/models/aws/__init__.py +15 -0
  180. agno/models/aws/bedrock.py +686 -451
  181. agno/models/aws/claude.py +190 -183
  182. agno/models/azure/__init__.py +18 -1
  183. agno/models/azure/ai_foundry.py +489 -0
  184. agno/models/azure/openai_chat.py +89 -40
  185. agno/models/base.py +2477 -550
  186. agno/models/cerebras/__init__.py +12 -0
  187. agno/models/cerebras/cerebras.py +565 -0
  188. agno/models/cerebras/cerebras_openai.py +131 -0
  189. agno/models/cohere/__init__.py +4 -0
  190. agno/models/cohere/chat.py +306 -492
  191. agno/models/cometapi/__init__.py +5 -0
  192. agno/models/cometapi/cometapi.py +74 -0
  193. agno/models/dashscope/__init__.py +5 -0
  194. agno/models/dashscope/dashscope.py +90 -0
  195. agno/models/deepinfra/__init__.py +5 -0
  196. agno/models/deepinfra/deepinfra.py +45 -0
  197. agno/models/deepseek/__init__.py +4 -0
  198. agno/models/deepseek/deepseek.py +110 -9
  199. agno/models/fireworks/__init__.py +4 -0
  200. agno/models/fireworks/fireworks.py +19 -22
  201. agno/models/google/__init__.py +3 -7
  202. agno/models/google/gemini.py +1717 -662
  203. agno/models/google/utils.py +22 -0
  204. agno/models/groq/__init__.py +4 -0
  205. agno/models/groq/groq.py +391 -666
  206. agno/models/huggingface/__init__.py +4 -0
  207. agno/models/huggingface/huggingface.py +266 -538
  208. agno/models/ibm/__init__.py +5 -0
  209. agno/models/ibm/watsonx.py +432 -0
  210. agno/models/internlm/__init__.py +3 -0
  211. agno/models/internlm/internlm.py +20 -3
  212. agno/models/langdb/__init__.py +1 -0
  213. agno/models/langdb/langdb.py +60 -0
  214. agno/models/litellm/__init__.py +14 -0
  215. agno/models/litellm/chat.py +503 -0
  216. agno/models/litellm/litellm_openai.py +42 -0
  217. agno/models/llama_cpp/__init__.py +5 -0
  218. agno/models/llama_cpp/llama_cpp.py +22 -0
  219. agno/models/lmstudio/__init__.py +5 -0
  220. agno/models/lmstudio/lmstudio.py +25 -0
  221. agno/models/message.py +361 -39
  222. agno/models/meta/__init__.py +12 -0
  223. agno/models/meta/llama.py +502 -0
  224. agno/models/meta/llama_openai.py +79 -0
  225. agno/models/metrics.py +120 -0
  226. agno/models/mistral/__init__.py +4 -0
  227. agno/models/mistral/mistral.py +293 -393
  228. agno/models/nebius/__init__.py +3 -0
  229. agno/models/nebius/nebius.py +53 -0
  230. agno/models/nexus/__init__.py +3 -0
  231. agno/models/nexus/nexus.py +22 -0
  232. agno/models/nvidia/__init__.py +4 -0
  233. agno/models/nvidia/nvidia.py +22 -3
  234. agno/models/ollama/__init__.py +4 -2
  235. agno/models/ollama/chat.py +257 -492
  236. agno/models/openai/__init__.py +7 -0
  237. agno/models/openai/chat.py +725 -770
  238. agno/models/openai/like.py +16 -2
  239. agno/models/openai/responses.py +1121 -0
  240. agno/models/openrouter/__init__.py +4 -0
  241. agno/models/openrouter/openrouter.py +62 -5
  242. agno/models/perplexity/__init__.py +5 -0
  243. agno/models/perplexity/perplexity.py +203 -0
  244. agno/models/portkey/__init__.py +3 -0
  245. agno/models/portkey/portkey.py +82 -0
  246. agno/models/requesty/__init__.py +5 -0
  247. agno/models/requesty/requesty.py +69 -0
  248. agno/models/response.py +177 -7
  249. agno/models/sambanova/__init__.py +4 -0
  250. agno/models/sambanova/sambanova.py +23 -4
  251. agno/models/siliconflow/__init__.py +5 -0
  252. agno/models/siliconflow/siliconflow.py +42 -0
  253. agno/models/together/__init__.py +4 -0
  254. agno/models/together/together.py +21 -164
  255. agno/models/utils.py +266 -0
  256. agno/models/vercel/__init__.py +3 -0
  257. agno/models/vercel/v0.py +43 -0
  258. agno/models/vertexai/__init__.py +0 -1
  259. agno/models/vertexai/claude.py +190 -0
  260. agno/models/vllm/__init__.py +3 -0
  261. agno/models/vllm/vllm.py +83 -0
  262. agno/models/xai/__init__.py +2 -0
  263. agno/models/xai/xai.py +111 -7
  264. agno/os/__init__.py +3 -0
  265. agno/os/app.py +1027 -0
  266. agno/os/auth.py +244 -0
  267. agno/os/config.py +126 -0
  268. agno/os/interfaces/__init__.py +1 -0
  269. agno/os/interfaces/a2a/__init__.py +3 -0
  270. agno/os/interfaces/a2a/a2a.py +42 -0
  271. agno/os/interfaces/a2a/router.py +249 -0
  272. agno/os/interfaces/a2a/utils.py +924 -0
  273. agno/os/interfaces/agui/__init__.py +3 -0
  274. agno/os/interfaces/agui/agui.py +47 -0
  275. agno/os/interfaces/agui/router.py +147 -0
  276. agno/os/interfaces/agui/utils.py +574 -0
  277. agno/os/interfaces/base.py +25 -0
  278. agno/os/interfaces/slack/__init__.py +3 -0
  279. agno/os/interfaces/slack/router.py +148 -0
  280. agno/os/interfaces/slack/security.py +30 -0
  281. agno/os/interfaces/slack/slack.py +47 -0
  282. agno/os/interfaces/whatsapp/__init__.py +3 -0
  283. agno/os/interfaces/whatsapp/router.py +210 -0
  284. agno/os/interfaces/whatsapp/security.py +55 -0
  285. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  286. agno/os/mcp.py +293 -0
  287. agno/os/middleware/__init__.py +9 -0
  288. agno/os/middleware/jwt.py +797 -0
  289. agno/os/router.py +258 -0
  290. agno/os/routers/__init__.py +3 -0
  291. agno/os/routers/agents/__init__.py +3 -0
  292. agno/os/routers/agents/router.py +599 -0
  293. agno/os/routers/agents/schema.py +261 -0
  294. agno/os/routers/evals/__init__.py +3 -0
  295. agno/os/routers/evals/evals.py +450 -0
  296. agno/os/routers/evals/schemas.py +174 -0
  297. agno/os/routers/evals/utils.py +231 -0
  298. agno/os/routers/health.py +31 -0
  299. agno/os/routers/home.py +52 -0
  300. agno/os/routers/knowledge/__init__.py +3 -0
  301. agno/os/routers/knowledge/knowledge.py +1008 -0
  302. agno/os/routers/knowledge/schemas.py +178 -0
  303. agno/os/routers/memory/__init__.py +3 -0
  304. agno/os/routers/memory/memory.py +661 -0
  305. agno/os/routers/memory/schemas.py +88 -0
  306. agno/os/routers/metrics/__init__.py +3 -0
  307. agno/os/routers/metrics/metrics.py +190 -0
  308. agno/os/routers/metrics/schemas.py +47 -0
  309. agno/os/routers/session/__init__.py +3 -0
  310. agno/os/routers/session/session.py +997 -0
  311. agno/os/routers/teams/__init__.py +3 -0
  312. agno/os/routers/teams/router.py +512 -0
  313. agno/os/routers/teams/schema.py +257 -0
  314. agno/os/routers/traces/__init__.py +3 -0
  315. agno/os/routers/traces/schemas.py +414 -0
  316. agno/os/routers/traces/traces.py +499 -0
  317. agno/os/routers/workflows/__init__.py +3 -0
  318. agno/os/routers/workflows/router.py +624 -0
  319. agno/os/routers/workflows/schema.py +75 -0
  320. agno/os/schema.py +534 -0
  321. agno/os/scopes.py +469 -0
  322. agno/{playground → os}/settings.py +7 -15
  323. agno/os/utils.py +973 -0
  324. agno/reasoning/anthropic.py +80 -0
  325. agno/reasoning/azure_ai_foundry.py +67 -0
  326. agno/reasoning/deepseek.py +63 -0
  327. agno/reasoning/default.py +97 -0
  328. agno/reasoning/gemini.py +73 -0
  329. agno/reasoning/groq.py +71 -0
  330. agno/reasoning/helpers.py +24 -1
  331. agno/reasoning/ollama.py +67 -0
  332. agno/reasoning/openai.py +86 -0
  333. agno/reasoning/step.py +2 -1
  334. agno/reasoning/vertexai.py +76 -0
  335. agno/run/__init__.py +6 -0
  336. agno/run/agent.py +822 -0
  337. agno/run/base.py +247 -0
  338. agno/run/cancel.py +81 -0
  339. agno/run/requirement.py +181 -0
  340. agno/run/team.py +767 -0
  341. agno/run/workflow.py +708 -0
  342. agno/session/__init__.py +10 -0
  343. agno/session/agent.py +260 -0
  344. agno/session/summary.py +265 -0
  345. agno/session/team.py +342 -0
  346. agno/session/workflow.py +501 -0
  347. agno/table.py +10 -0
  348. agno/team/__init__.py +37 -0
  349. agno/team/team.py +9536 -0
  350. agno/tools/__init__.py +7 -0
  351. agno/tools/agentql.py +120 -0
  352. agno/tools/airflow.py +22 -12
  353. agno/tools/api.py +122 -0
  354. agno/tools/apify.py +276 -83
  355. agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
  356. agno/tools/aws_lambda.py +28 -7
  357. agno/tools/aws_ses.py +66 -0
  358. agno/tools/baidusearch.py +11 -4
  359. agno/tools/bitbucket.py +292 -0
  360. agno/tools/brandfetch.py +213 -0
  361. agno/tools/bravesearch.py +106 -0
  362. agno/tools/brightdata.py +367 -0
  363. agno/tools/browserbase.py +209 -0
  364. agno/tools/calcom.py +32 -23
  365. agno/tools/calculator.py +24 -37
  366. agno/tools/cartesia.py +187 -0
  367. agno/tools/{clickup_tool.py → clickup.py} +17 -28
  368. agno/tools/confluence.py +91 -26
  369. agno/tools/crawl4ai.py +139 -43
  370. agno/tools/csv_toolkit.py +28 -22
  371. agno/tools/dalle.py +36 -22
  372. agno/tools/daytona.py +475 -0
  373. agno/tools/decorator.py +169 -14
  374. agno/tools/desi_vocal.py +23 -11
  375. agno/tools/discord.py +32 -29
  376. agno/tools/docker.py +716 -0
  377. agno/tools/duckdb.py +76 -81
  378. agno/tools/duckduckgo.py +43 -40
  379. agno/tools/e2b.py +703 -0
  380. agno/tools/eleven_labs.py +65 -54
  381. agno/tools/email.py +13 -5
  382. agno/tools/evm.py +129 -0
  383. agno/tools/exa.py +324 -42
  384. agno/tools/fal.py +39 -35
  385. agno/tools/file.py +196 -30
  386. agno/tools/file_generation.py +356 -0
  387. agno/tools/financial_datasets.py +288 -0
  388. agno/tools/firecrawl.py +108 -33
  389. agno/tools/function.py +960 -122
  390. agno/tools/giphy.py +34 -12
  391. agno/tools/github.py +1294 -97
  392. agno/tools/gmail.py +922 -0
  393. agno/tools/google_bigquery.py +117 -0
  394. agno/tools/google_drive.py +271 -0
  395. agno/tools/google_maps.py +253 -0
  396. agno/tools/googlecalendar.py +607 -107
  397. agno/tools/googlesheets.py +377 -0
  398. agno/tools/hackernews.py +20 -12
  399. agno/tools/jina.py +24 -14
  400. agno/tools/jira.py +48 -19
  401. agno/tools/knowledge.py +218 -0
  402. agno/tools/linear.py +82 -43
  403. agno/tools/linkup.py +58 -0
  404. agno/tools/local_file_system.py +15 -7
  405. agno/tools/lumalab.py +41 -26
  406. agno/tools/mcp/__init__.py +10 -0
  407. agno/tools/mcp/mcp.py +331 -0
  408. agno/tools/mcp/multi_mcp.py +347 -0
  409. agno/tools/mcp/params.py +24 -0
  410. agno/tools/mcp_toolbox.py +284 -0
  411. agno/tools/mem0.py +193 -0
  412. agno/tools/memory.py +419 -0
  413. agno/tools/mlx_transcribe.py +11 -9
  414. agno/tools/models/azure_openai.py +190 -0
  415. agno/tools/models/gemini.py +203 -0
  416. agno/tools/models/groq.py +158 -0
  417. agno/tools/models/morph.py +186 -0
  418. agno/tools/models/nebius.py +124 -0
  419. agno/tools/models_labs.py +163 -82
  420. agno/tools/moviepy_video.py +18 -13
  421. agno/tools/nano_banana.py +151 -0
  422. agno/tools/neo4j.py +134 -0
  423. agno/tools/newspaper.py +15 -4
  424. agno/tools/newspaper4k.py +19 -6
  425. agno/tools/notion.py +204 -0
  426. agno/tools/openai.py +181 -17
  427. agno/tools/openbb.py +27 -20
  428. agno/tools/opencv.py +321 -0
  429. agno/tools/openweather.py +233 -0
  430. agno/tools/oxylabs.py +385 -0
  431. agno/tools/pandas.py +25 -15
  432. agno/tools/parallel.py +314 -0
  433. agno/tools/postgres.py +238 -185
  434. agno/tools/pubmed.py +125 -13
  435. agno/tools/python.py +48 -35
  436. agno/tools/reasoning.py +283 -0
  437. agno/tools/reddit.py +207 -29
  438. agno/tools/redshift.py +406 -0
  439. agno/tools/replicate.py +69 -26
  440. agno/tools/resend.py +11 -6
  441. agno/tools/scrapegraph.py +179 -19
  442. agno/tools/searxng.py +23 -31
  443. agno/tools/serpapi.py +15 -10
  444. agno/tools/serper.py +255 -0
  445. agno/tools/shell.py +23 -12
  446. agno/tools/shopify.py +1519 -0
  447. agno/tools/slack.py +56 -14
  448. agno/tools/sleep.py +8 -6
  449. agno/tools/spider.py +35 -11
  450. agno/tools/spotify.py +919 -0
  451. agno/tools/sql.py +34 -19
  452. agno/tools/tavily.py +158 -8
  453. agno/tools/telegram.py +18 -8
  454. agno/tools/todoist.py +218 -0
  455. agno/tools/toolkit.py +134 -9
  456. agno/tools/trafilatura.py +388 -0
  457. agno/tools/trello.py +25 -28
  458. agno/tools/twilio.py +18 -9
  459. agno/tools/user_control_flow.py +78 -0
  460. agno/tools/valyu.py +228 -0
  461. agno/tools/visualization.py +467 -0
  462. agno/tools/webbrowser.py +28 -0
  463. agno/tools/webex.py +76 -0
  464. agno/tools/website.py +23 -19
  465. agno/tools/webtools.py +45 -0
  466. agno/tools/whatsapp.py +286 -0
  467. agno/tools/wikipedia.py +28 -19
  468. agno/tools/workflow.py +285 -0
  469. agno/tools/{twitter.py → x.py} +142 -46
  470. agno/tools/yfinance.py +41 -39
  471. agno/tools/youtube.py +34 -17
  472. agno/tools/zendesk.py +15 -5
  473. agno/tools/zep.py +454 -0
  474. agno/tools/zoom.py +86 -37
  475. agno/tracing/__init__.py +12 -0
  476. agno/tracing/exporter.py +157 -0
  477. agno/tracing/schemas.py +276 -0
  478. agno/tracing/setup.py +111 -0
  479. agno/utils/agent.py +938 -0
  480. agno/utils/audio.py +37 -1
  481. agno/utils/certs.py +27 -0
  482. agno/utils/code_execution.py +11 -0
  483. agno/utils/common.py +103 -20
  484. agno/utils/cryptography.py +22 -0
  485. agno/utils/dttm.py +33 -0
  486. agno/utils/events.py +700 -0
  487. agno/utils/functions.py +107 -37
  488. agno/utils/gemini.py +426 -0
  489. agno/utils/hooks.py +171 -0
  490. agno/utils/http.py +185 -0
  491. agno/utils/json_schema.py +159 -37
  492. agno/utils/knowledge.py +36 -0
  493. agno/utils/location.py +19 -0
  494. agno/utils/log.py +221 -8
  495. agno/utils/mcp.py +214 -0
  496. agno/utils/media.py +335 -14
  497. agno/utils/merge_dict.py +22 -1
  498. agno/utils/message.py +77 -2
  499. agno/utils/models/ai_foundry.py +50 -0
  500. agno/utils/models/claude.py +373 -0
  501. agno/utils/models/cohere.py +94 -0
  502. agno/utils/models/llama.py +85 -0
  503. agno/utils/models/mistral.py +100 -0
  504. agno/utils/models/openai_responses.py +140 -0
  505. agno/utils/models/schema_utils.py +153 -0
  506. agno/utils/models/watsonx.py +41 -0
  507. agno/utils/openai.py +257 -0
  508. agno/utils/pickle.py +1 -1
  509. agno/utils/pprint.py +124 -8
  510. agno/utils/print_response/agent.py +930 -0
  511. agno/utils/print_response/team.py +1914 -0
  512. agno/utils/print_response/workflow.py +1668 -0
  513. agno/utils/prompts.py +111 -0
  514. agno/utils/reasoning.py +108 -0
  515. agno/utils/response.py +163 -0
  516. agno/utils/serialize.py +32 -0
  517. agno/utils/shell.py +4 -4
  518. agno/utils/streamlit.py +487 -0
  519. agno/utils/string.py +204 -51
  520. agno/utils/team.py +139 -0
  521. agno/utils/timer.py +9 -2
  522. agno/utils/tokens.py +657 -0
  523. agno/utils/tools.py +19 -1
  524. agno/utils/whatsapp.py +305 -0
  525. agno/utils/yaml_io.py +3 -3
  526. agno/vectordb/__init__.py +2 -0
  527. agno/vectordb/base.py +87 -9
  528. agno/vectordb/cassandra/__init__.py +5 -1
  529. agno/vectordb/cassandra/cassandra.py +383 -27
  530. agno/vectordb/chroma/__init__.py +4 -0
  531. agno/vectordb/chroma/chromadb.py +748 -83
  532. agno/vectordb/clickhouse/__init__.py +7 -1
  533. agno/vectordb/clickhouse/clickhousedb.py +554 -53
  534. agno/vectordb/couchbase/__init__.py +3 -0
  535. agno/vectordb/couchbase/couchbase.py +1446 -0
  536. agno/vectordb/lancedb/__init__.py +5 -0
  537. agno/vectordb/lancedb/lance_db.py +730 -98
  538. agno/vectordb/langchaindb/__init__.py +5 -0
  539. agno/vectordb/langchaindb/langchaindb.py +163 -0
  540. agno/vectordb/lightrag/__init__.py +5 -0
  541. agno/vectordb/lightrag/lightrag.py +388 -0
  542. agno/vectordb/llamaindex/__init__.py +3 -0
  543. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  544. agno/vectordb/milvus/__init__.py +3 -0
  545. agno/vectordb/milvus/milvus.py +966 -78
  546. agno/vectordb/mongodb/__init__.py +9 -1
  547. agno/vectordb/mongodb/mongodb.py +1175 -172
  548. agno/vectordb/pgvector/__init__.py +8 -0
  549. agno/vectordb/pgvector/pgvector.py +599 -115
  550. agno/vectordb/pineconedb/__init__.py +5 -1
  551. agno/vectordb/pineconedb/pineconedb.py +406 -43
  552. agno/vectordb/qdrant/__init__.py +4 -0
  553. agno/vectordb/qdrant/qdrant.py +914 -61
  554. agno/vectordb/redis/__init__.py +9 -0
  555. agno/vectordb/redis/redisdb.py +682 -0
  556. agno/vectordb/singlestore/__init__.py +8 -1
  557. agno/vectordb/singlestore/singlestore.py +771 -0
  558. agno/vectordb/surrealdb/__init__.py +3 -0
  559. agno/vectordb/surrealdb/surrealdb.py +663 -0
  560. agno/vectordb/upstashdb/__init__.py +5 -0
  561. agno/vectordb/upstashdb/upstashdb.py +718 -0
  562. agno/vectordb/weaviate/__init__.py +8 -0
  563. agno/vectordb/weaviate/index.py +15 -0
  564. agno/vectordb/weaviate/weaviate.py +1009 -0
  565. agno/workflow/__init__.py +23 -1
  566. agno/workflow/agent.py +299 -0
  567. agno/workflow/condition.py +759 -0
  568. agno/workflow/loop.py +756 -0
  569. agno/workflow/parallel.py +853 -0
  570. agno/workflow/router.py +723 -0
  571. agno/workflow/step.py +1564 -0
  572. agno/workflow/steps.py +613 -0
  573. agno/workflow/types.py +556 -0
  574. agno/workflow/workflow.py +4327 -514
  575. agno-2.3.13.dist-info/METADATA +639 -0
  576. agno-2.3.13.dist-info/RECORD +613 -0
  577. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
  578. agno-2.3.13.dist-info/licenses/LICENSE +201 -0
  579. agno/api/playground.py +0 -91
  580. agno/api/schemas/playground.py +0 -22
  581. agno/api/schemas/user.py +0 -22
  582. agno/api/schemas/workspace.py +0 -46
  583. agno/api/user.py +0 -160
  584. agno/api/workspace.py +0 -151
  585. agno/cli/auth_server.py +0 -118
  586. agno/cli/config.py +0 -275
  587. agno/cli/console.py +0 -88
  588. agno/cli/credentials.py +0 -23
  589. agno/cli/entrypoint.py +0 -571
  590. agno/cli/operator.py +0 -355
  591. agno/cli/settings.py +0 -85
  592. agno/cli/ws/ws_cli.py +0 -817
  593. agno/constants.py +0 -13
  594. agno/document/__init__.py +0 -1
  595. agno/document/chunking/semantic.py +0 -47
  596. agno/document/chunking/strategy.py +0 -31
  597. agno/document/reader/__init__.py +0 -1
  598. agno/document/reader/arxiv_reader.py +0 -41
  599. agno/document/reader/base.py +0 -22
  600. agno/document/reader/csv_reader.py +0 -84
  601. agno/document/reader/docx_reader.py +0 -46
  602. agno/document/reader/firecrawl_reader.py +0 -99
  603. agno/document/reader/json_reader.py +0 -43
  604. agno/document/reader/pdf_reader.py +0 -219
  605. agno/document/reader/s3/pdf_reader.py +0 -46
  606. agno/document/reader/s3/text_reader.py +0 -51
  607. agno/document/reader/text_reader.py +0 -41
  608. agno/document/reader/website_reader.py +0 -175
  609. agno/document/reader/youtube_reader.py +0 -50
  610. agno/embedder/__init__.py +0 -1
  611. agno/embedder/azure_openai.py +0 -86
  612. agno/embedder/cohere.py +0 -72
  613. agno/embedder/fastembed.py +0 -37
  614. agno/embedder/google.py +0 -73
  615. agno/embedder/huggingface.py +0 -54
  616. agno/embedder/mistral.py +0 -80
  617. agno/embedder/ollama.py +0 -57
  618. agno/embedder/openai.py +0 -74
  619. agno/embedder/sentence_transformer.py +0 -38
  620. agno/embedder/voyageai.py +0 -64
  621. agno/eval/perf.py +0 -201
  622. agno/file/__init__.py +0 -1
  623. agno/file/file.py +0 -16
  624. agno/file/local/csv.py +0 -32
  625. agno/file/local/txt.py +0 -19
  626. agno/infra/app.py +0 -240
  627. agno/infra/base.py +0 -144
  628. agno/infra/context.py +0 -20
  629. agno/infra/db_app.py +0 -52
  630. agno/infra/resource.py +0 -205
  631. agno/infra/resources.py +0 -55
  632. agno/knowledge/agent.py +0 -230
  633. agno/knowledge/arxiv.py +0 -22
  634. agno/knowledge/combined.py +0 -22
  635. agno/knowledge/csv.py +0 -28
  636. agno/knowledge/csv_url.py +0 -19
  637. agno/knowledge/document.py +0 -20
  638. agno/knowledge/docx.py +0 -30
  639. agno/knowledge/json.py +0 -28
  640. agno/knowledge/langchain.py +0 -71
  641. agno/knowledge/llamaindex.py +0 -66
  642. agno/knowledge/pdf.py +0 -28
  643. agno/knowledge/pdf_url.py +0 -26
  644. agno/knowledge/s3/base.py +0 -60
  645. agno/knowledge/s3/pdf.py +0 -21
  646. agno/knowledge/s3/text.py +0 -23
  647. agno/knowledge/text.py +0 -30
  648. agno/knowledge/website.py +0 -88
  649. agno/knowledge/wikipedia.py +0 -31
  650. agno/knowledge/youtube.py +0 -22
  651. agno/memory/agent.py +0 -392
  652. agno/memory/classifier.py +0 -104
  653. agno/memory/db/__init__.py +0 -1
  654. agno/memory/db/base.py +0 -42
  655. agno/memory/db/mongodb.py +0 -189
  656. agno/memory/db/postgres.py +0 -203
  657. agno/memory/db/sqlite.py +0 -193
  658. agno/memory/memory.py +0 -15
  659. agno/memory/row.py +0 -36
  660. agno/memory/summarizer.py +0 -192
  661. agno/memory/summary.py +0 -19
  662. agno/memory/workflow.py +0 -38
  663. agno/models/google/gemini_openai.py +0 -26
  664. agno/models/ollama/hermes.py +0 -221
  665. agno/models/ollama/tools.py +0 -362
  666. agno/models/vertexai/gemini.py +0 -595
  667. agno/playground/__init__.py +0 -3
  668. agno/playground/async_router.py +0 -421
  669. agno/playground/deploy.py +0 -249
  670. agno/playground/operator.py +0 -92
  671. agno/playground/playground.py +0 -91
  672. agno/playground/schemas.py +0 -76
  673. agno/playground/serve.py +0 -55
  674. agno/playground/sync_router.py +0 -405
  675. agno/reasoning/agent.py +0 -68
  676. agno/run/response.py +0 -112
  677. agno/storage/agent/__init__.py +0 -0
  678. agno/storage/agent/base.py +0 -38
  679. agno/storage/agent/dynamodb.py +0 -350
  680. agno/storage/agent/json.py +0 -92
  681. agno/storage/agent/mongodb.py +0 -228
  682. agno/storage/agent/postgres.py +0 -367
  683. agno/storage/agent/session.py +0 -79
  684. agno/storage/agent/singlestore.py +0 -303
  685. agno/storage/agent/sqlite.py +0 -357
  686. agno/storage/agent/yaml.py +0 -93
  687. agno/storage/workflow/__init__.py +0 -0
  688. agno/storage/workflow/base.py +0 -40
  689. agno/storage/workflow/mongodb.py +0 -233
  690. agno/storage/workflow/postgres.py +0 -366
  691. agno/storage/workflow/session.py +0 -60
  692. agno/storage/workflow/sqlite.py +0 -359
  693. agno/tools/googlesearch.py +0 -88
  694. agno/utils/defaults.py +0 -57
  695. agno/utils/filesystem.py +0 -39
  696. agno/utils/git.py +0 -52
  697. agno/utils/json_io.py +0 -30
  698. agno/utils/load_env.py +0 -19
  699. agno/utils/py_io.py +0 -19
  700. agno/utils/pyproject.py +0 -18
  701. agno/utils/resource_filter.py +0 -31
  702. agno/vectordb/singlestore/s2vectordb.py +0 -390
  703. agno/vectordb/singlestore/s2vectordb2.py +0 -355
  704. agno/workspace/__init__.py +0 -0
  705. agno/workspace/config.py +0 -325
  706. agno/workspace/enums.py +0 -6
  707. agno/workspace/helpers.py +0 -48
  708. agno/workspace/operator.py +0 -758
  709. agno/workspace/settings.py +0 -63
  710. agno-0.1.2.dist-info/LICENSE +0 -375
  711. agno-0.1.2.dist-info/METADATA +0 -502
  712. agno-0.1.2.dist-info/RECORD +0 -352
  713. agno-0.1.2.dist-info/entry_points.txt +0 -3
  714. /agno/{cli → db/migrations}/__init__.py +0 -0
  715. /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
  716. /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
  717. /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
  718. /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
  719. /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
  720. /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
  721. /agno/{reranker → utils/models}/__init__.py +0 -0
  722. /agno/{storage → utils/print_response}/__init__.py +0 -0
  723. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/tools/github.py CHANGED
@@ -1,12 +1,13 @@
1
1
  import json
2
- import os
3
- from typing import List, Optional
2
+ from os import getenv
3
+ from typing import Any, List, Optional
4
4
 
5
5
  from agno.tools import Toolkit
6
- from agno.utils.log import logger
6
+ from agno.utils.log import log_debug, logger
7
7
 
8
8
  try:
9
9
  from github import Auth, Github, GithubException
10
+
10
11
  except ImportError:
11
12
  raise ImportError("`PyGithub` not installed. Please install using `pip install pygithub`")
12
13
 
@@ -16,74 +17,102 @@ class GithubTools(Toolkit):
16
17
  self,
17
18
  access_token: Optional[str] = None,
18
19
  base_url: Optional[str] = None,
19
- search_repositories: bool = True,
20
- list_repositories: bool = True,
21
- get_repository: bool = True,
22
- list_pull_requests: bool = True,
23
- get_pull_request: bool = True,
24
- get_pull_request_changes: bool = True,
25
- create_issue: bool = True,
26
- create_repository: bool = True,
27
- get_repository_languages: bool = True,
20
+ **kwargs,
28
21
  ):
29
- super().__init__(name="github")
30
-
31
- self.access_token = access_token or os.getenv("GITHUB_ACCESS_TOKEN")
22
+ self.access_token = access_token or getenv("GITHUB_ACCESS_TOKEN")
32
23
  self.base_url = base_url
33
24
 
34
25
  self.g = self.authenticate()
35
26
 
36
- if search_repositories:
37
- self.register(self.search_repositories)
38
- if list_repositories:
39
- self.register(self.list_repositories)
40
- if get_repository:
41
- self.register(self.get_repository)
42
- if list_pull_requests:
43
- self.register(self.list_pull_requests)
44
- if get_pull_request:
45
- self.register(self.get_pull_request)
46
- if get_pull_request_changes:
47
- self.register(self.get_pull_request_changes)
48
- if create_issue:
49
- self.register(self.create_issue)
50
- if create_repository:
51
- self.register(self.create_repository)
52
-
53
- if get_repository_languages:
54
- self.register(self.get_repository_languages)
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)
55
70
 
56
71
  def authenticate(self):
57
72
  """Authenticate with GitHub using the provided access token."""
58
-
59
73
  if not self.access_token: # Fixes lint type error
60
74
  raise ValueError("GitHub access token is required")
61
75
 
62
76
  auth = Auth.Token(self.access_token)
63
77
  if self.base_url:
64
- logger.debug(f"Authenticating with GitHub Enterprise at {self.base_url}")
78
+ log_debug(f"Authenticating with GitHub Enterprise at {self.base_url}")
65
79
  return Github(base_url=self.base_url, auth=auth)
66
80
  else:
67
- logger.debug("Authenticating with public GitHub")
81
+ log_debug("Authenticating with public GitHub")
68
82
  return Github(auth=auth)
69
83
 
70
- def search_repositories(self, query: str, sort: str = "stars", order: str = "desc", per_page: int = 5) -> str:
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:
71
92
  """Search for repositories on GitHub.
72
93
 
94
+ Note: GitHub's Search API has a maximum limit of 1000 results per query.
95
+
73
96
  Args:
74
97
  query (str): The search query keywords.
75
98
  sort (str, optional): The field to sort results by. Can be 'stars', 'forks', or 'updated'. Defaults to 'stars'.
76
99
  order (str, optional): The order of results. Can be 'asc' or 'desc'. Defaults to 'desc'.
77
- per_page (int, optional): Number of results per page. Defaults to 5.
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.
78
102
 
79
103
  Returns:
80
104
  A JSON-formatted string containing a list of repositories matching the search query.
81
105
  """
82
- logger.debug(f"Searching repositories with query: '{query}'")
106
+ log_debug(f"Searching repositories with query: '{query}', page: {page}, per_page: {per_page}")
83
107
  try:
108
+ # Ensure per_page doesn't exceed GitHub's max of 100
109
+ per_page = min(per_page, 100)
110
+
84
111
  repositories = self.g.search_repositories(query=query, sort=sort, order=order)
112
+
113
+ # Get the specified page of results
85
114
  repo_list = []
86
- for repo in repositories[:per_page]:
115
+ for repo in repositories.get_page(page - 1):
87
116
  repo_info = {
88
117
  "full_name": repo.full_name,
89
118
  "description": repo.description,
@@ -93,7 +122,12 @@ class GithubTools(Toolkit):
93
122
  "language": repo.language,
94
123
  }
95
124
  repo_list.append(repo_info)
125
+
126
+ if len(repo_list) >= per_page:
127
+ break
128
+
96
129
  return json.dumps(repo_list, indent=2)
130
+
97
131
  except GithubException as e:
98
132
  logger.error(f"Error searching repositories: {e}")
99
133
  return json.dumps({"error": str(e)})
@@ -104,7 +138,7 @@ class GithubTools(Toolkit):
104
138
  Returns:
105
139
  A JSON-formatted string containing a list of repository names.
106
140
  """
107
- logger.debug("Listing repositories")
141
+ log_debug("Listing repositories")
108
142
  try:
109
143
  repos = self.g.get_user().get_repos()
110
144
  repo_names = [repo.full_name for repo in repos]
@@ -133,12 +167,12 @@ class GithubTools(Toolkit):
133
167
  Returns:
134
168
  A JSON-formatted string containing the created repository details.
135
169
  """
136
- logger.debug(f"Creating repository: {name}")
170
+ log_debug(f"Creating repository: {name}")
137
171
  try:
138
172
  description = description if description is not None else ""
139
173
 
140
174
  if organization:
141
- logger.debug(f"Creating in organization: {organization}")
175
+ log_debug(f"Creating in organization: {organization}")
142
176
  org = self.g.get_organization(organization)
143
177
  repo = org.create_repo(
144
178
  name=name,
@@ -174,7 +208,7 @@ class GithubTools(Toolkit):
174
208
  Returns:
175
209
  A JSON-formatted string containing repository details.
176
210
  """
177
- logger.debug(f"Getting repository: {repo_name}")
211
+ log_debug(f"Getting repository: {repo_name}")
178
212
  try:
179
213
  repo = self.g.get_repo(repo_name)
180
214
  repo_info = {
@@ -202,7 +236,7 @@ class GithubTools(Toolkit):
202
236
  Returns:
203
237
  A JSON-formatted string containing the list of languages.
204
238
  """
205
- logger.debug(f"Getting languages for repository: {repo_name}")
239
+ log_debug(f"Getting languages for repository: {repo_name}")
206
240
  try:
207
241
  repo = self.g.get_repo(repo_name)
208
242
  languages = repo.get_languages()
@@ -211,34 +245,44 @@ class GithubTools(Toolkit):
211
245
  logger.error(f"Error getting repository languages: {e}")
212
246
  return json.dumps({"error": str(e)})
213
247
 
214
- def list_pull_requests(self, repo_name: str, state: str = "open") -> str:
215
- """List pull requests for a repository.
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.
216
257
 
217
258
  Args:
218
259
  repo_name (str): The full name of the repository (e.g., 'owner/repo').
219
- state (str, optional): The state of the PRs to list ('open', 'closed', 'all'). Defaults to 'open'.
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.
220
264
 
221
265
  Returns:
222
- A JSON-formatted string containing a list of pull requests.
266
+ A JSON-formatted string containing the count of pull requests.
223
267
  """
224
- logger.debug(f"Listing pull requests for repository: {repo_name} with state: {state}")
268
+ log_debug(f"Counting pull requests for repository: {repo_name} with state: {state}")
225
269
  try:
226
270
  repo = self.g.get_repo(repo_name)
227
- pulls = repo.get_pulls(state=state)
228
- pr_list = []
229
- for pr in pulls:
230
- pr_info = {
231
- "number": pr.number,
232
- "title": pr.title,
233
- "user": pr.user.login,
234
- "created_at": pr.created_at.isoformat(),
235
- "state": pr.state,
236
- "url": pr.html_url,
237
- }
238
- pr_list.append(pr_info)
239
- return json.dumps(pr_list, indent=2)
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)
240
284
  except GithubException as e:
241
- logger.error(f"Error listing pull requests: {e}")
285
+ logger.error(f"Error counting pull requests: {e}")
242
286
  return json.dumps({"error": str(e)})
243
287
 
244
288
  def get_pull_request(self, repo_name: str, pr_number: int) -> str:
@@ -248,10 +292,11 @@ class GithubTools(Toolkit):
248
292
  repo_name (str): The full name of the repository (e.g., 'owner/repo').
249
293
  pr_number (int): The number of the pull request.
250
294
 
295
+
251
296
  Returns:
252
297
  A JSON-formatted string containing pull request details.
253
298
  """
254
- logger.debug(f"Getting pull request #{pr_number} for repository: {repo_name}")
299
+ log_debug(f"Getting pull request #{pr_number} for repository: {repo_name}")
255
300
  try:
256
301
  repo = self.g.get_repo(repo_name)
257
302
  pr = repo.get_pull(pr_number)
@@ -282,7 +327,7 @@ class GithubTools(Toolkit):
282
327
  Returns:
283
328
  A JSON-formatted string containing the list of changed files.
284
329
  """
285
- logger.debug(f"Getting changes for pull request #{pr_number} in repository: {repo_name}")
330
+ log_debug(f"Getting changes for pull request #{pr_number} in repository: {repo_name}")
286
331
  try:
287
332
  repo = self.g.get_repo(repo_name)
288
333
  pr = repo.get_pull(pr_number)
@@ -316,10 +361,10 @@ class GithubTools(Toolkit):
316
361
  Returns:
317
362
  A JSON-formatted string containing the created issue details.
318
363
  """
319
- logger.debug(f"Creating issue in repository: {repo_name}")
364
+ log_debug(f"Creating issue in repository: {repo_name}")
320
365
  try:
321
366
  repo = self.g.get_repo(repo_name)
322
- issue = repo.create_issue(title=title, body=body)
367
+ issue = repo.create_issue(title=title, body=body) # type: ignore
323
368
  issue_info = {
324
369
  "id": issue.id,
325
370
  "number": issue.number,
@@ -335,34 +380,63 @@ class GithubTools(Toolkit):
335
380
  logger.error(f"Error creating issue: {e}")
336
381
  return json.dumps({"error": str(e)})
337
382
 
338
- def list_issues(self, repo_name: str, state: str = "open") -> str:
339
- """List issues for a repository.
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.
340
385
 
341
386
  Args:
342
387
  repo_name (str): The full name of the repository (e.g., 'owner/repo').
343
388
  state (str, optional): The state of issues to list ('open', 'closed', 'all'). Defaults to 'open'.
344
-
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.
345
391
  Returns:
346
- A JSON-formatted string containing a list of issues.
392
+ A JSON-formatted string containing a list of issues with pagination metadata.
347
393
  """
348
- logger.debug(f"Listing issues for repository: {repo_name} with state: {state}")
394
+ log_debug(f"Listing issues for repository: {repo_name} with state: {state}, page: {page}, per_page: {per_page}")
349
395
  try:
350
396
  repo = self.g.get_repo(repo_name)
397
+
351
398
  issues = repo.get_issues(state=state)
399
+
352
400
  # Filter out pull requests after fetching issues
353
- filtered_issues = [issue for issue in issues if not issue.pull_request]
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
354
418
  issue_list = []
355
- for issue in filtered_issues:
356
- issue_info = {
357
- "number": issue.number,
358
- "title": issue.title,
359
- "user": issue.user.login,
360
- "created_at": issue.created_at.isoformat(),
361
- "state": issue.state,
362
- "url": issue.html_url,
363
- }
364
- issue_list.append(issue_info)
365
- return json.dumps(issue_list, indent=2)
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)
366
440
  except GithubException as e:
367
441
  logger.error(f"Error listing issues: {e}")
368
442
  return json.dumps({"error": str(e)})
@@ -377,7 +451,7 @@ class GithubTools(Toolkit):
377
451
  Returns:
378
452
  A JSON-formatted string containing issue details.
379
453
  """
380
- logger.debug(f"Getting issue #{issue_number} for repository: {repo_name}")
454
+ log_debug(f"Getting issue #{issue_number} for repository: {repo_name}")
381
455
  try:
382
456
  repo = self.g.get_repo(repo_name)
383
457
  issue = repo.get_issue(number=issue_number)
@@ -409,7 +483,7 @@ class GithubTools(Toolkit):
409
483
  Returns:
410
484
  A JSON-formatted string containing the comment details.
411
485
  """
412
- logger.debug(f"Adding comment to issue #{issue_number} in repository: {repo_name}")
486
+ log_debug(f"Adding comment to issue #{issue_number} in repository: {repo_name}")
413
487
  try:
414
488
  repo = self.g.get_repo(repo_name)
415
489
  issue = repo.get_issue(number=issue_number)
@@ -436,7 +510,7 @@ class GithubTools(Toolkit):
436
510
  Returns:
437
511
  A JSON-formatted string confirming the issue is closed.
438
512
  """
439
- logger.debug(f"Closing issue #{issue_number} in repository: {repo_name}")
513
+ log_debug(f"Closing issue #{issue_number} in repository: {repo_name}")
440
514
  try:
441
515
  repo = self.g.get_repo(repo_name)
442
516
  issue = repo.get_issue(number=issue_number)
@@ -456,7 +530,7 @@ class GithubTools(Toolkit):
456
530
  Returns:
457
531
  A JSON-formatted string confirming the issue is reopened.
458
532
  """
459
- logger.debug(f"Reopening issue #{issue_number} in repository: {repo_name}")
533
+ log_debug(f"Reopening issue #{issue_number} in repository: {repo_name}")
460
534
  try:
461
535
  repo = self.g.get_repo(repo_name)
462
536
  issue = repo.get_issue(number=issue_number)
@@ -477,7 +551,7 @@ class GithubTools(Toolkit):
477
551
  Returns:
478
552
  A JSON-formatted string confirming the assignees.
479
553
  """
480
- logger.debug(f"Assigning users to issue #{issue_number} in repository: {repo_name}")
554
+ log_debug(f"Assigning users to issue #{issue_number} in repository: {repo_name}")
481
555
  try:
482
556
  repo = self.g.get_repo(repo_name)
483
557
  issue = repo.get_issue(number=issue_number)
@@ -498,12 +572,15 @@ class GithubTools(Toolkit):
498
572
  Returns:
499
573
  A JSON-formatted string confirming the labels.
500
574
  """
501
- logger.debug(f"Labeling issue #{issue_number} in repository: {repo_name}")
575
+ log_debug(f"Labeling issue #{issue_number} in repository: {repo_name}")
502
576
  try:
503
577
  repo = self.g.get_repo(repo_name)
504
578
  issue = repo.get_issue(number=issue_number)
505
579
  issue.edit(labels=labels)
506
- return json.dumps({"message": f"Labels {labels} added to issue #{issue_number}."}, indent=2)
580
+ return json.dumps(
581
+ {"message": f"Labels {labels} added to issue #{issue_number}."},
582
+ indent=2,
583
+ )
507
584
  except GithubException as e:
508
585
  logger.error(f"Error labeling issue: {e}")
509
586
  return json.dumps({"error": str(e)})
@@ -518,7 +595,7 @@ class GithubTools(Toolkit):
518
595
  Returns:
519
596
  A JSON-formatted string containing a list of comments.
520
597
  """
521
- logger.debug(f"Listing comments for issue #{issue_number} in repository: {repo_name}")
598
+ log_debug(f"Listing comments for issue #{issue_number} in repository: {repo_name}")
522
599
  try:
523
600
  repo = self.g.get_repo(repo_name)
524
601
  issue = repo.get_issue(number=issue_number)
@@ -539,7 +616,11 @@ class GithubTools(Toolkit):
539
616
  return json.dumps({"error": str(e)})
540
617
 
541
618
  def edit_issue(
542
- self, repo_name: str, issue_number: int, title: Optional[str] = None, body: Optional[str] = None
619
+ self,
620
+ repo_name: str,
621
+ issue_number: int,
622
+ title: Optional[str] = None,
623
+ body: Optional[str] = None,
543
624
  ) -> str:
544
625
  """Edit the title or body of an issue.
545
626
 
@@ -552,12 +633,1128 @@ class GithubTools(Toolkit):
552
633
  Returns:
553
634
  A JSON-formatted string confirming the issue has been updated.
554
635
  """
555
- logger.debug(f"Editing issue #{issue_number} in repository: {repo_name}")
636
+ log_debug(f"Editing issue #{issue_number} in repository: {repo_name}")
556
637
  try:
557
638
  repo = self.g.get_repo(repo_name)
558
639
  issue = repo.get_issue(number=issue_number)
559
- issue.edit(title=title, body=body)
640
+ issue.edit(title=title, body=body) # type: ignore
560
641
  return json.dumps({"message": f"Issue #{issue_number} updated."}, indent=2)
561
642
  except GithubException as e:
562
643
  logger.error(f"Error editing issue: {e}")
563
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)})