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.
- agno/__init__.py +8 -0
- agno/agent/__init__.py +44 -5
- agno/agent/agent.py +10531 -2975
- agno/api/agent.py +14 -53
- agno/api/api.py +7 -46
- agno/api/evals.py +22 -0
- agno/api/os.py +17 -0
- agno/api/routes.py +6 -25
- agno/api/schemas/__init__.py +9 -0
- agno/api/schemas/agent.py +6 -9
- agno/api/schemas/evals.py +16 -0
- agno/api/schemas/os.py +14 -0
- agno/api/schemas/team.py +10 -10
- agno/api/schemas/utils.py +21 -0
- agno/api/schemas/workflows.py +16 -0
- agno/api/settings.py +53 -0
- agno/api/team.py +22 -26
- agno/api/workflow.py +28 -0
- agno/cloud/aws/base.py +214 -0
- agno/cloud/aws/s3/__init__.py +2 -0
- agno/cloud/aws/s3/api_client.py +43 -0
- agno/cloud/aws/s3/bucket.py +195 -0
- agno/cloud/aws/s3/object.py +57 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/__init__.py +24 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +946 -0
- agno/db/dynamo/__init__.py +3 -0
- agno/db/dynamo/dynamo.py +2781 -0
- agno/db/dynamo/schemas.py +442 -0
- agno/db/dynamo/utils.py +743 -0
- agno/db/firestore/__init__.py +3 -0
- agno/db/firestore/firestore.py +2379 -0
- agno/db/firestore/schemas.py +181 -0
- agno/db/firestore/utils.py +376 -0
- agno/db/gcs_json/__init__.py +3 -0
- agno/db/gcs_json/gcs_json_db.py +1791 -0
- agno/db/gcs_json/utils.py +228 -0
- agno/db/in_memory/__init__.py +3 -0
- agno/db/in_memory/in_memory_db.py +1312 -0
- agno/db/in_memory/utils.py +230 -0
- agno/db/json/__init__.py +3 -0
- agno/db/json/json_db.py +1777 -0
- agno/db/json/utils.py +230 -0
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +635 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +17 -0
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +2597 -0
- agno/db/mongo/schemas.py +119 -0
- agno/db/mongo/utils.py +276 -0
- agno/db/mysql/__init__.py +4 -0
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +2923 -0
- agno/db/mysql/schemas.py +186 -0
- agno/db/mysql/utils.py +488 -0
- agno/db/postgres/__init__.py +4 -0
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +2870 -0
- agno/db/postgres/schemas.py +187 -0
- agno/db/postgres/utils.py +442 -0
- agno/db/redis/__init__.py +3 -0
- agno/db/redis/redis.py +2141 -0
- agno/db/redis/schemas.py +159 -0
- agno/db/redis/utils.py +346 -0
- agno/db/schemas/__init__.py +4 -0
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +34 -0
- agno/db/schemas/knowledge.py +40 -0
- agno/db/schemas/memory.py +61 -0
- agno/db/singlestore/__init__.py +3 -0
- agno/db/singlestore/schemas.py +179 -0
- agno/db/singlestore/singlestore.py +2877 -0
- agno/db/singlestore/utils.py +384 -0
- agno/db/sqlite/__init__.py +4 -0
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +181 -0
- agno/db/sqlite/sqlite.py +2908 -0
- agno/db/sqlite/utils.py +429 -0
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +118 -0
- agno/eval/__init__.py +24 -0
- agno/eval/accuracy.py +666 -276
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +779 -0
- agno/eval/reliability.py +241 -62
- agno/eval/utils.py +120 -0
- agno/exceptions.py +143 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/__init__.py +3 -0
- agno/integrations/discord/client.py +203 -0
- agno/knowledge/__init__.py +5 -1
- agno/{document → knowledge}/chunking/agentic.py +22 -14
- agno/{document → knowledge}/chunking/document.py +2 -2
- agno/{document → knowledge}/chunking/fixed.py +7 -6
- agno/knowledge/chunking/markdown.py +151 -0
- agno/{document → knowledge}/chunking/recursive.py +15 -3
- agno/knowledge/chunking/row.py +39 -0
- agno/knowledge/chunking/semantic.py +91 -0
- agno/knowledge/chunking/strategy.py +165 -0
- agno/knowledge/content.py +74 -0
- agno/knowledge/document/__init__.py +5 -0
- agno/{document → knowledge/document}/base.py +12 -2
- agno/knowledge/embedder/__init__.py +5 -0
- agno/knowledge/embedder/aws_bedrock.py +343 -0
- agno/knowledge/embedder/azure_openai.py +210 -0
- agno/{embedder → knowledge/embedder}/base.py +8 -0
- agno/knowledge/embedder/cohere.py +323 -0
- agno/knowledge/embedder/fastembed.py +62 -0
- agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
- agno/knowledge/embedder/google.py +258 -0
- agno/knowledge/embedder/huggingface.py +94 -0
- agno/knowledge/embedder/jina.py +182 -0
- agno/knowledge/embedder/langdb.py +22 -0
- agno/knowledge/embedder/mistral.py +206 -0
- agno/knowledge/embedder/nebius.py +13 -0
- agno/knowledge/embedder/ollama.py +154 -0
- agno/knowledge/embedder/openai.py +195 -0
- agno/knowledge/embedder/sentence_transformer.py +63 -0
- agno/{embedder → knowledge/embedder}/together.py +1 -1
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +165 -0
- agno/knowledge/knowledge.py +3006 -0
- agno/knowledge/reader/__init__.py +7 -0
- agno/knowledge/reader/arxiv_reader.py +81 -0
- agno/knowledge/reader/base.py +95 -0
- agno/knowledge/reader/csv_reader.py +164 -0
- agno/knowledge/reader/docx_reader.py +82 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/firecrawl_reader.py +201 -0
- agno/knowledge/reader/json_reader.py +88 -0
- agno/knowledge/reader/markdown_reader.py +137 -0
- agno/knowledge/reader/pdf_reader.py +431 -0
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +313 -0
- agno/knowledge/reader/s3_reader.py +89 -0
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +127 -0
- agno/knowledge/reader/web_search_reader.py +325 -0
- agno/knowledge/reader/website_reader.py +455 -0
- agno/knowledge/reader/wikipedia_reader.py +91 -0
- agno/knowledge/reader/youtube_reader.py +78 -0
- agno/knowledge/remote_content/remote_content.py +88 -0
- agno/knowledge/reranker/__init__.py +3 -0
- agno/{reranker → knowledge/reranker}/base.py +1 -1
- agno/{reranker → knowledge/reranker}/cohere.py +2 -2
- agno/knowledge/reranker/infinity.py +195 -0
- agno/knowledge/reranker/sentence_transformer.py +54 -0
- agno/knowledge/types.py +39 -0
- agno/knowledge/utils.py +234 -0
- agno/media.py +439 -95
- agno/memory/__init__.py +16 -3
- agno/memory/manager.py +1474 -123
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/__init__.py +5 -0
- agno/models/aimlapi/aimlapi.py +62 -0
- agno/models/anthropic/__init__.py +4 -0
- agno/models/anthropic/claude.py +960 -496
- agno/models/aws/__init__.py +15 -0
- agno/models/aws/bedrock.py +686 -451
- agno/models/aws/claude.py +190 -183
- agno/models/azure/__init__.py +18 -1
- agno/models/azure/ai_foundry.py +489 -0
- agno/models/azure/openai_chat.py +89 -40
- agno/models/base.py +2477 -550
- agno/models/cerebras/__init__.py +12 -0
- agno/models/cerebras/cerebras.py +565 -0
- agno/models/cerebras/cerebras_openai.py +131 -0
- agno/models/cohere/__init__.py +4 -0
- agno/models/cohere/chat.py +306 -492
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +74 -0
- agno/models/dashscope/__init__.py +5 -0
- agno/models/dashscope/dashscope.py +90 -0
- agno/models/deepinfra/__init__.py +5 -0
- agno/models/deepinfra/deepinfra.py +45 -0
- agno/models/deepseek/__init__.py +4 -0
- agno/models/deepseek/deepseek.py +110 -9
- agno/models/fireworks/__init__.py +4 -0
- agno/models/fireworks/fireworks.py +19 -22
- agno/models/google/__init__.py +3 -7
- agno/models/google/gemini.py +1717 -662
- agno/models/google/utils.py +22 -0
- agno/models/groq/__init__.py +4 -0
- agno/models/groq/groq.py +391 -666
- agno/models/huggingface/__init__.py +4 -0
- agno/models/huggingface/huggingface.py +266 -538
- agno/models/ibm/__init__.py +5 -0
- agno/models/ibm/watsonx.py +432 -0
- agno/models/internlm/__init__.py +3 -0
- agno/models/internlm/internlm.py +20 -3
- agno/models/langdb/__init__.py +1 -0
- agno/models/langdb/langdb.py +60 -0
- agno/models/litellm/__init__.py +14 -0
- agno/models/litellm/chat.py +503 -0
- agno/models/litellm/litellm_openai.py +42 -0
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/lmstudio/__init__.py +5 -0
- agno/models/lmstudio/lmstudio.py +25 -0
- agno/models/message.py +361 -39
- agno/models/meta/__init__.py +12 -0
- agno/models/meta/llama.py +502 -0
- agno/models/meta/llama_openai.py +79 -0
- agno/models/metrics.py +120 -0
- agno/models/mistral/__init__.py +4 -0
- agno/models/mistral/mistral.py +293 -393
- agno/models/nebius/__init__.py +3 -0
- agno/models/nebius/nebius.py +53 -0
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/__init__.py +4 -0
- agno/models/nvidia/nvidia.py +22 -3
- agno/models/ollama/__init__.py +4 -2
- agno/models/ollama/chat.py +257 -492
- agno/models/openai/__init__.py +7 -0
- agno/models/openai/chat.py +725 -770
- agno/models/openai/like.py +16 -2
- agno/models/openai/responses.py +1121 -0
- agno/models/openrouter/__init__.py +4 -0
- agno/models/openrouter/openrouter.py +62 -5
- agno/models/perplexity/__init__.py +5 -0
- agno/models/perplexity/perplexity.py +203 -0
- agno/models/portkey/__init__.py +3 -0
- agno/models/portkey/portkey.py +82 -0
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +69 -0
- agno/models/response.py +177 -7
- agno/models/sambanova/__init__.py +4 -0
- agno/models/sambanova/sambanova.py +23 -4
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +42 -0
- agno/models/together/__init__.py +4 -0
- agno/models/together/together.py +21 -164
- agno/models/utils.py +266 -0
- agno/models/vercel/__init__.py +3 -0
- agno/models/vercel/v0.py +43 -0
- agno/models/vertexai/__init__.py +0 -1
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/__init__.py +3 -0
- agno/models/vllm/vllm.py +83 -0
- agno/models/xai/__init__.py +2 -0
- agno/models/xai/xai.py +111 -7
- agno/os/__init__.py +3 -0
- agno/os/app.py +1027 -0
- agno/os/auth.py +244 -0
- agno/os/config.py +126 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +249 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/__init__.py +3 -0
- agno/os/interfaces/agui/agui.py +47 -0
- agno/os/interfaces/agui/router.py +147 -0
- agno/os/interfaces/agui/utils.py +574 -0
- agno/os/interfaces/base.py +25 -0
- agno/os/interfaces/slack/__init__.py +3 -0
- agno/os/interfaces/slack/router.py +148 -0
- agno/os/interfaces/slack/security.py +30 -0
- agno/os/interfaces/slack/slack.py +47 -0
- agno/os/interfaces/whatsapp/__init__.py +3 -0
- agno/os/interfaces/whatsapp/router.py +210 -0
- agno/os/interfaces/whatsapp/security.py +55 -0
- agno/os/interfaces/whatsapp/whatsapp.py +36 -0
- agno/os/mcp.py +293 -0
- agno/os/middleware/__init__.py +9 -0
- agno/os/middleware/jwt.py +797 -0
- agno/os/router.py +258 -0
- agno/os/routers/__init__.py +3 -0
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/__init__.py +3 -0
- agno/os/routers/evals/evals.py +450 -0
- agno/os/routers/evals/schemas.py +174 -0
- agno/os/routers/evals/utils.py +231 -0
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/__init__.py +3 -0
- agno/os/routers/knowledge/knowledge.py +1008 -0
- agno/os/routers/knowledge/schemas.py +178 -0
- agno/os/routers/memory/__init__.py +3 -0
- agno/os/routers/memory/memory.py +661 -0
- agno/os/routers/memory/schemas.py +88 -0
- agno/os/routers/metrics/__init__.py +3 -0
- agno/os/routers/metrics/metrics.py +190 -0
- agno/os/routers/metrics/schemas.py +47 -0
- agno/os/routers/session/__init__.py +3 -0
- agno/os/routers/session/session.py +997 -0
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +534 -0
- agno/os/scopes.py +469 -0
- agno/{playground → os}/settings.py +7 -15
- agno/os/utils.py +973 -0
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +67 -0
- agno/reasoning/deepseek.py +63 -0
- agno/reasoning/default.py +97 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +71 -0
- agno/reasoning/helpers.py +24 -1
- agno/reasoning/ollama.py +67 -0
- agno/reasoning/openai.py +86 -0
- agno/reasoning/step.py +2 -1
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +822 -0
- agno/run/base.py +247 -0
- agno/run/cancel.py +81 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +767 -0
- agno/run/workflow.py +708 -0
- agno/session/__init__.py +10 -0
- agno/session/agent.py +260 -0
- agno/session/summary.py +265 -0
- agno/session/team.py +342 -0
- agno/session/workflow.py +501 -0
- agno/table.py +10 -0
- agno/team/__init__.py +37 -0
- agno/team/team.py +9536 -0
- agno/tools/__init__.py +7 -0
- agno/tools/agentql.py +120 -0
- agno/tools/airflow.py +22 -12
- agno/tools/api.py +122 -0
- agno/tools/apify.py +276 -83
- agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
- agno/tools/aws_lambda.py +28 -7
- agno/tools/aws_ses.py +66 -0
- agno/tools/baidusearch.py +11 -4
- agno/tools/bitbucket.py +292 -0
- agno/tools/brandfetch.py +213 -0
- agno/tools/bravesearch.py +106 -0
- agno/tools/brightdata.py +367 -0
- agno/tools/browserbase.py +209 -0
- agno/tools/calcom.py +32 -23
- agno/tools/calculator.py +24 -37
- agno/tools/cartesia.py +187 -0
- agno/tools/{clickup_tool.py → clickup.py} +17 -28
- agno/tools/confluence.py +91 -26
- agno/tools/crawl4ai.py +139 -43
- agno/tools/csv_toolkit.py +28 -22
- agno/tools/dalle.py +36 -22
- agno/tools/daytona.py +475 -0
- agno/tools/decorator.py +169 -14
- agno/tools/desi_vocal.py +23 -11
- agno/tools/discord.py +32 -29
- agno/tools/docker.py +716 -0
- agno/tools/duckdb.py +76 -81
- agno/tools/duckduckgo.py +43 -40
- agno/tools/e2b.py +703 -0
- agno/tools/eleven_labs.py +65 -54
- agno/tools/email.py +13 -5
- agno/tools/evm.py +129 -0
- agno/tools/exa.py +324 -42
- agno/tools/fal.py +39 -35
- agno/tools/file.py +196 -30
- agno/tools/file_generation.py +356 -0
- agno/tools/financial_datasets.py +288 -0
- agno/tools/firecrawl.py +108 -33
- agno/tools/function.py +960 -122
- agno/tools/giphy.py +34 -12
- agno/tools/github.py +1294 -97
- agno/tools/gmail.py +922 -0
- agno/tools/google_bigquery.py +117 -0
- agno/tools/google_drive.py +271 -0
- agno/tools/google_maps.py +253 -0
- agno/tools/googlecalendar.py +607 -107
- agno/tools/googlesheets.py +377 -0
- agno/tools/hackernews.py +20 -12
- agno/tools/jina.py +24 -14
- agno/tools/jira.py +48 -19
- agno/tools/knowledge.py +218 -0
- agno/tools/linear.py +82 -43
- agno/tools/linkup.py +58 -0
- agno/tools/local_file_system.py +15 -7
- agno/tools/lumalab.py +41 -26
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +193 -0
- agno/tools/memory.py +419 -0
- agno/tools/mlx_transcribe.py +11 -9
- agno/tools/models/azure_openai.py +190 -0
- agno/tools/models/gemini.py +203 -0
- agno/tools/models/groq.py +158 -0
- agno/tools/models/morph.py +186 -0
- agno/tools/models/nebius.py +124 -0
- agno/tools/models_labs.py +163 -82
- agno/tools/moviepy_video.py +18 -13
- agno/tools/nano_banana.py +151 -0
- agno/tools/neo4j.py +134 -0
- agno/tools/newspaper.py +15 -4
- agno/tools/newspaper4k.py +19 -6
- agno/tools/notion.py +204 -0
- agno/tools/openai.py +181 -17
- agno/tools/openbb.py +27 -20
- agno/tools/opencv.py +321 -0
- agno/tools/openweather.py +233 -0
- agno/tools/oxylabs.py +385 -0
- agno/tools/pandas.py +25 -15
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +238 -185
- agno/tools/pubmed.py +125 -13
- agno/tools/python.py +48 -35
- agno/tools/reasoning.py +283 -0
- agno/tools/reddit.py +207 -29
- agno/tools/redshift.py +406 -0
- agno/tools/replicate.py +69 -26
- agno/tools/resend.py +11 -6
- agno/tools/scrapegraph.py +179 -19
- agno/tools/searxng.py +23 -31
- agno/tools/serpapi.py +15 -10
- agno/tools/serper.py +255 -0
- agno/tools/shell.py +23 -12
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +56 -14
- agno/tools/sleep.py +8 -6
- agno/tools/spider.py +35 -11
- agno/tools/spotify.py +919 -0
- agno/tools/sql.py +34 -19
- agno/tools/tavily.py +158 -8
- agno/tools/telegram.py +18 -8
- agno/tools/todoist.py +218 -0
- agno/tools/toolkit.py +134 -9
- agno/tools/trafilatura.py +388 -0
- agno/tools/trello.py +25 -28
- agno/tools/twilio.py +18 -9
- agno/tools/user_control_flow.py +78 -0
- agno/tools/valyu.py +228 -0
- agno/tools/visualization.py +467 -0
- agno/tools/webbrowser.py +28 -0
- agno/tools/webex.py +76 -0
- agno/tools/website.py +23 -19
- agno/tools/webtools.py +45 -0
- agno/tools/whatsapp.py +286 -0
- agno/tools/wikipedia.py +28 -19
- agno/tools/workflow.py +285 -0
- agno/tools/{twitter.py → x.py} +142 -46
- agno/tools/yfinance.py +41 -39
- agno/tools/youtube.py +34 -17
- agno/tools/zendesk.py +15 -5
- agno/tools/zep.py +454 -0
- agno/tools/zoom.py +86 -37
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/audio.py +37 -1
- agno/utils/certs.py +27 -0
- agno/utils/code_execution.py +11 -0
- agno/utils/common.py +103 -20
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +700 -0
- agno/utils/functions.py +107 -37
- agno/utils/gemini.py +426 -0
- agno/utils/hooks.py +171 -0
- agno/utils/http.py +185 -0
- agno/utils/json_schema.py +159 -37
- agno/utils/knowledge.py +36 -0
- agno/utils/location.py +19 -0
- agno/utils/log.py +221 -8
- agno/utils/mcp.py +214 -0
- agno/utils/media.py +335 -14
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +77 -2
- agno/utils/models/ai_foundry.py +50 -0
- agno/utils/models/claude.py +373 -0
- agno/utils/models/cohere.py +94 -0
- agno/utils/models/llama.py +85 -0
- agno/utils/models/mistral.py +100 -0
- agno/utils/models/openai_responses.py +140 -0
- agno/utils/models/schema_utils.py +153 -0
- agno/utils/models/watsonx.py +41 -0
- agno/utils/openai.py +257 -0
- agno/utils/pickle.py +1 -1
- agno/utils/pprint.py +124 -8
- agno/utils/print_response/agent.py +930 -0
- agno/utils/print_response/team.py +1914 -0
- agno/utils/print_response/workflow.py +1668 -0
- agno/utils/prompts.py +111 -0
- agno/utils/reasoning.py +108 -0
- agno/utils/response.py +163 -0
- agno/utils/serialize.py +32 -0
- agno/utils/shell.py +4 -4
- agno/utils/streamlit.py +487 -0
- agno/utils/string.py +204 -51
- agno/utils/team.py +139 -0
- agno/utils/timer.py +9 -2
- agno/utils/tokens.py +657 -0
- agno/utils/tools.py +19 -1
- agno/utils/whatsapp.py +305 -0
- agno/utils/yaml_io.py +3 -3
- agno/vectordb/__init__.py +2 -0
- agno/vectordb/base.py +87 -9
- agno/vectordb/cassandra/__init__.py +5 -1
- agno/vectordb/cassandra/cassandra.py +383 -27
- agno/vectordb/chroma/__init__.py +4 -0
- agno/vectordb/chroma/chromadb.py +748 -83
- agno/vectordb/clickhouse/__init__.py +7 -1
- agno/vectordb/clickhouse/clickhousedb.py +554 -53
- agno/vectordb/couchbase/__init__.py +3 -0
- agno/vectordb/couchbase/couchbase.py +1446 -0
- agno/vectordb/lancedb/__init__.py +5 -0
- agno/vectordb/lancedb/lance_db.py +730 -98
- agno/vectordb/langchaindb/__init__.py +5 -0
- agno/vectordb/langchaindb/langchaindb.py +163 -0
- agno/vectordb/lightrag/__init__.py +5 -0
- agno/vectordb/lightrag/lightrag.py +388 -0
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +166 -0
- agno/vectordb/milvus/__init__.py +3 -0
- agno/vectordb/milvus/milvus.py +966 -78
- agno/vectordb/mongodb/__init__.py +9 -1
- agno/vectordb/mongodb/mongodb.py +1175 -172
- agno/vectordb/pgvector/__init__.py +8 -0
- agno/vectordb/pgvector/pgvector.py +599 -115
- agno/vectordb/pineconedb/__init__.py +5 -1
- agno/vectordb/pineconedb/pineconedb.py +406 -43
- agno/vectordb/qdrant/__init__.py +4 -0
- agno/vectordb/qdrant/qdrant.py +914 -61
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/__init__.py +8 -1
- agno/vectordb/singlestore/singlestore.py +771 -0
- agno/vectordb/surrealdb/__init__.py +3 -0
- agno/vectordb/surrealdb/surrealdb.py +663 -0
- agno/vectordb/upstashdb/__init__.py +5 -0
- agno/vectordb/upstashdb/upstashdb.py +718 -0
- agno/vectordb/weaviate/__init__.py +8 -0
- agno/vectordb/weaviate/index.py +15 -0
- agno/vectordb/weaviate/weaviate.py +1009 -0
- agno/workflow/__init__.py +23 -1
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +759 -0
- agno/workflow/loop.py +756 -0
- agno/workflow/parallel.py +853 -0
- agno/workflow/router.py +723 -0
- agno/workflow/step.py +1564 -0
- agno/workflow/steps.py +613 -0
- agno/workflow/types.py +556 -0
- agno/workflow/workflow.py +4327 -514
- agno-2.3.13.dist-info/METADATA +639 -0
- agno-2.3.13.dist-info/RECORD +613 -0
- {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
- agno-2.3.13.dist-info/licenses/LICENSE +201 -0
- agno/api/playground.py +0 -91
- agno/api/schemas/playground.py +0 -22
- agno/api/schemas/user.py +0 -22
- agno/api/schemas/workspace.py +0 -46
- agno/api/user.py +0 -160
- agno/api/workspace.py +0 -151
- agno/cli/auth_server.py +0 -118
- agno/cli/config.py +0 -275
- agno/cli/console.py +0 -88
- agno/cli/credentials.py +0 -23
- agno/cli/entrypoint.py +0 -571
- agno/cli/operator.py +0 -355
- agno/cli/settings.py +0 -85
- agno/cli/ws/ws_cli.py +0 -817
- agno/constants.py +0 -13
- agno/document/__init__.py +0 -1
- agno/document/chunking/semantic.py +0 -47
- agno/document/chunking/strategy.py +0 -31
- agno/document/reader/__init__.py +0 -1
- agno/document/reader/arxiv_reader.py +0 -41
- agno/document/reader/base.py +0 -22
- agno/document/reader/csv_reader.py +0 -84
- agno/document/reader/docx_reader.py +0 -46
- agno/document/reader/firecrawl_reader.py +0 -99
- agno/document/reader/json_reader.py +0 -43
- agno/document/reader/pdf_reader.py +0 -219
- agno/document/reader/s3/pdf_reader.py +0 -46
- agno/document/reader/s3/text_reader.py +0 -51
- agno/document/reader/text_reader.py +0 -41
- agno/document/reader/website_reader.py +0 -175
- agno/document/reader/youtube_reader.py +0 -50
- agno/embedder/__init__.py +0 -1
- agno/embedder/azure_openai.py +0 -86
- agno/embedder/cohere.py +0 -72
- agno/embedder/fastembed.py +0 -37
- agno/embedder/google.py +0 -73
- agno/embedder/huggingface.py +0 -54
- agno/embedder/mistral.py +0 -80
- agno/embedder/ollama.py +0 -57
- agno/embedder/openai.py +0 -74
- agno/embedder/sentence_transformer.py +0 -38
- agno/embedder/voyageai.py +0 -64
- agno/eval/perf.py +0 -201
- agno/file/__init__.py +0 -1
- agno/file/file.py +0 -16
- agno/file/local/csv.py +0 -32
- agno/file/local/txt.py +0 -19
- agno/infra/app.py +0 -240
- agno/infra/base.py +0 -144
- agno/infra/context.py +0 -20
- agno/infra/db_app.py +0 -52
- agno/infra/resource.py +0 -205
- agno/infra/resources.py +0 -55
- agno/knowledge/agent.py +0 -230
- agno/knowledge/arxiv.py +0 -22
- agno/knowledge/combined.py +0 -22
- agno/knowledge/csv.py +0 -28
- agno/knowledge/csv_url.py +0 -19
- agno/knowledge/document.py +0 -20
- agno/knowledge/docx.py +0 -30
- agno/knowledge/json.py +0 -28
- agno/knowledge/langchain.py +0 -71
- agno/knowledge/llamaindex.py +0 -66
- agno/knowledge/pdf.py +0 -28
- agno/knowledge/pdf_url.py +0 -26
- agno/knowledge/s3/base.py +0 -60
- agno/knowledge/s3/pdf.py +0 -21
- agno/knowledge/s3/text.py +0 -23
- agno/knowledge/text.py +0 -30
- agno/knowledge/website.py +0 -88
- agno/knowledge/wikipedia.py +0 -31
- agno/knowledge/youtube.py +0 -22
- agno/memory/agent.py +0 -392
- agno/memory/classifier.py +0 -104
- agno/memory/db/__init__.py +0 -1
- agno/memory/db/base.py +0 -42
- agno/memory/db/mongodb.py +0 -189
- agno/memory/db/postgres.py +0 -203
- agno/memory/db/sqlite.py +0 -193
- agno/memory/memory.py +0 -15
- agno/memory/row.py +0 -36
- agno/memory/summarizer.py +0 -192
- agno/memory/summary.py +0 -19
- agno/memory/workflow.py +0 -38
- agno/models/google/gemini_openai.py +0 -26
- agno/models/ollama/hermes.py +0 -221
- agno/models/ollama/tools.py +0 -362
- agno/models/vertexai/gemini.py +0 -595
- agno/playground/__init__.py +0 -3
- agno/playground/async_router.py +0 -421
- agno/playground/deploy.py +0 -249
- agno/playground/operator.py +0 -92
- agno/playground/playground.py +0 -91
- agno/playground/schemas.py +0 -76
- agno/playground/serve.py +0 -55
- agno/playground/sync_router.py +0 -405
- agno/reasoning/agent.py +0 -68
- agno/run/response.py +0 -112
- agno/storage/agent/__init__.py +0 -0
- agno/storage/agent/base.py +0 -38
- agno/storage/agent/dynamodb.py +0 -350
- agno/storage/agent/json.py +0 -92
- agno/storage/agent/mongodb.py +0 -228
- agno/storage/agent/postgres.py +0 -367
- agno/storage/agent/session.py +0 -79
- agno/storage/agent/singlestore.py +0 -303
- agno/storage/agent/sqlite.py +0 -357
- agno/storage/agent/yaml.py +0 -93
- agno/storage/workflow/__init__.py +0 -0
- agno/storage/workflow/base.py +0 -40
- agno/storage/workflow/mongodb.py +0 -233
- agno/storage/workflow/postgres.py +0 -366
- agno/storage/workflow/session.py +0 -60
- agno/storage/workflow/sqlite.py +0 -359
- agno/tools/googlesearch.py +0 -88
- agno/utils/defaults.py +0 -57
- agno/utils/filesystem.py +0 -39
- agno/utils/git.py +0 -52
- agno/utils/json_io.py +0 -30
- agno/utils/load_env.py +0 -19
- agno/utils/py_io.py +0 -19
- agno/utils/pyproject.py +0 -18
- agno/utils/resource_filter.py +0 -31
- agno/vectordb/singlestore/s2vectordb.py +0 -390
- agno/vectordb/singlestore/s2vectordb2.py +0 -355
- agno/workspace/__init__.py +0 -0
- agno/workspace/config.py +0 -325
- agno/workspace/enums.py +0 -6
- agno/workspace/helpers.py +0 -48
- agno/workspace/operator.py +0 -758
- agno/workspace/settings.py +0 -63
- agno-0.1.2.dist-info/LICENSE +0 -375
- agno-0.1.2.dist-info/METADATA +0 -502
- agno-0.1.2.dist-info/RECORD +0 -352
- agno-0.1.2.dist-info/entry_points.txt +0 -3
- /agno/{cli → db/migrations}/__init__.py +0 -0
- /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
- /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
- /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
- /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
- /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
- /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
- /agno/{reranker → utils/models}/__init__.py +0 -0
- /agno/{storage → utils/print_response}/__init__.py +0 -0
- {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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
self.
|
|
40
|
-
|
|
41
|
-
self.
|
|
42
|
-
|
|
43
|
-
self.
|
|
44
|
-
|
|
45
|
-
self.
|
|
46
|
-
|
|
47
|
-
self.
|
|
48
|
-
|
|
49
|
-
self.
|
|
50
|
-
|
|
51
|
-
self.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
log_debug("Authenticating with public GitHub")
|
|
68
82
|
return Github(auth=auth)
|
|
69
83
|
|
|
70
|
-
def search_repositories(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
215
|
-
|
|
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
|
|
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
|
|
266
|
+
A JSON-formatted string containing the count of pull requests.
|
|
223
267
|
"""
|
|
224
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return json.dumps(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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)})
|