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