mergescope 0.1.0__tar.gz

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.
@@ -0,0 +1,31 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ id-token: write
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ python-version: ["3.11", "3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v6
19
+ - run: uv python install ${{ matrix.python-version }}
20
+ - run: uv sync --dev
21
+ - run: uv run pytest tests/ -v
22
+
23
+ publish:
24
+ needs: test
25
+ runs-on: ubuntu-latest
26
+ environment: pypi
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: astral-sh/setup-uv@v6
30
+ - run: uv build
31
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ *.egg
6
+ dist/
7
+ build/
8
+ .eggs/
9
+ *.whl
10
+ .venv/
11
+ venv/
12
+ env/
13
+ .env
14
+ *.log
15
+ .mypy_cache/
16
+ .pytest_cache/
17
+ .ruff_cache/
18
+ htmlcov/
19
+ .coverage
20
+ BUILD.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Martin Caminoa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: mergescope
3
+ Version: 0.1.0
4
+ Summary: Audit merged PRs against Jira tickets using a LangGraph agent and MCP tools
5
+ Project-URL: Homepage, https://github.com/martin5211/MergeScope
6
+ Project-URL: Repository, https://github.com/martin5211/MergeScope
7
+ Project-URL: Issues, https://github.com/martin5211/MergeScope/issues
8
+ Author: Martin Caminoa
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: langchain-anthropic<2.0,>=0.3
22
+ Requires-Dist: langchain-aws<2.0,>=0.2
23
+ Requires-Dist: langchain-mcp-adapters<2.0,>=0.1
24
+ Requires-Dist: langchain-openai<2.0,>=0.3
25
+ Requires-Dist: langgraph<2.0,>=0.4
26
+ Requires-Dist: pyyaml<7.0,>=6.0
27
+ Requires-Dist: rich<15.0,>=13.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # MergeScope
31
+
32
+ Audits merged PRs against Jira tickets. Extracts Jira IDs from PR titles, descriptions, branch names, and commit messages, then validates each ticket's fixVersion against your expected release.
33
+
34
+ Powered by a LangGraph agent that talks to GitHub and Jira through MCP servers. Works standalone or with Amazon Q.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ uvx mergescope --help
40
+ ```
41
+
42
+ Or permanently:
43
+
44
+ ```bash
45
+ uv tool install mergescope
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```bash
51
+ mergescope \
52
+ -f 2026-03-01 \
53
+ -t 2026-03-31 \
54
+ -v "1.2.0" \
55
+ -r company/backend
56
+ ```
57
+
58
+ | Flag | Description |
59
+ |---|---|
60
+ | `-f`, `--from-date` | Start date (YYYY-MM-DD) |
61
+ | `-t`, `--to-date` | End date (YYYY-MM-DD) |
62
+ | `-v`, `--fix-version` | Expected Jira fixVersion |
63
+ | `-r`, `--repo` | GitHub repo (owner/repo) |
64
+ | `-c`, `--config` | Config file (default: `mergescope.yaml`) |
65
+ | `--mcp-config` | Amazon Q MCP config path |
66
+ | `--llm-provider` | `anthropic`, `bedrock`, `copilot`, or `local` |
67
+ | `--llm-model` | Model ID |
68
+ | `--llm-base-url` | API base URL (copilot/local) |
69
+ | `--language` | Response language (e.g. Spanish) |
70
+ | `--verbose` | Debug logging |
71
+
72
+ ### Multilingual prompts
73
+
74
+ Set a custom prompt in any language via `mergescope.yaml`:
75
+
76
+ ```yaml
77
+ language: "es"
78
+ response_language: "Spanish"
79
+ prompt_template: |
80
+ Necesito auditar tickets mergeados entre {from_date} y {to_date}.
81
+ La fixVersion esperada es {fix_version}.
82
+ Extrae los IDs de JIRA desde los merge commits del repositorio {repo}
83
+ y valida su fixVersion.
84
+ ```
85
+
86
+ ## Configuration
87
+
88
+ Add a `mergescope.yaml` in your project root:
89
+
90
+ ```yaml
91
+ repo: "nice/java-project"
92
+ jira_project_prefix: "PROJ"
93
+ jira_base_url: "https://company.jira.com"
94
+
95
+ llm:
96
+ provider: "anthropic" # anthropic, bedrock, copilot, local
97
+ model: "claude-sonnet-4-6-20250620"
98
+
99
+ mcp:
100
+ servers:
101
+ github:
102
+ name: "github-mcp-server"
103
+ jira:
104
+ name: "atlassian-mcp-server"
105
+ ```
106
+
107
+ ### LLM providers
108
+
109
+ | Provider | Model example | Notes |
110
+ |---|---|---|
111
+ | `anthropic` | `claude-sonnet-4-6-20250620` | Uses `ANTHROPIC_API_KEY` |
112
+ | `bedrock` | `us.anthropic.claude-sonnet-4-20250514-v1:0` | Uses AWS credentials |
113
+ | `copilot` | `claude-sonnet-4` | Set `base_url` to Copilot endpoint |
114
+ | `local` | `qwen3-8b` | LM Studio, Ollama, or any OpenAI-compatible server |
115
+
116
+ ### MCP servers
117
+
118
+ Two modes — pick what fits your setup:
119
+
120
+ **With Amazon Q** (default): just set server names. MergeScope finds them in `~/.aws/amazonq/mcp.json` or `.amazonq/mcp.json`.
121
+
122
+ **Standalone** (no Amazon Q): define `command`, `args`, and `env` directly:
123
+
124
+ ```yaml
125
+ mcp:
126
+ servers:
127
+ github:
128
+ name: "github-mcp-server"
129
+ command: "npx"
130
+ args: ["-y", "@anthropic/github-mcp-server"]
131
+ env:
132
+ GITHUB_TOKEN: "ghp_..."
133
+ jira:
134
+ name: "atlassian-mcp-server"
135
+ command: "npx"
136
+ args: ["-y", "@anthropic/atlassian-mcp-server"]
137
+ env:
138
+ JIRA_API_TOKEN: "..."
139
+ JIRA_URL: "https://company.jira.com"
140
+ ```
141
+
142
+ You can mix both — some servers inline, others from Amazon Q.
143
+
144
+ ### Environment variables
145
+
146
+ `MERGESCOPE_REPO`, `MERGESCOPE_LLM_PROVIDER`, `MERGESCOPE_LLM_MODEL`, `MERGESCOPE_LLM_BASE_URL`, `MERGESCOPE_LLM_API_KEY`, `MERGESCOPE_JIRA_BASE_URL`, `MERGESCOPE_LANGUAGE`
147
+
148
+ ## Requirements
149
+
150
+ - Python 3.11+
151
+ - GitHub and Jira MCP servers (standalone or via Amazon Q)
152
+ - An LLM: Anthropic API key, AWS credentials, GitHub Copilot, or a local server
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ git clone https://github.com/martin5211/MergeScope.git
158
+ cd MergeScope
159
+ uv venv && uv pip install -e .
160
+ pytest
161
+ ```
@@ -0,0 +1,132 @@
1
+ # MergeScope
2
+
3
+ Audits merged PRs against Jira tickets. Extracts Jira IDs from PR titles, descriptions, branch names, and commit messages, then validates each ticket's fixVersion against your expected release.
4
+
5
+ Powered by a LangGraph agent that talks to GitHub and Jira through MCP servers. Works standalone or with Amazon Q.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ uvx mergescope --help
11
+ ```
12
+
13
+ Or permanently:
14
+
15
+ ```bash
16
+ uv tool install mergescope
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ mergescope \
23
+ -f 2026-03-01 \
24
+ -t 2026-03-31 \
25
+ -v "1.2.0" \
26
+ -r company/backend
27
+ ```
28
+
29
+ | Flag | Description |
30
+ |---|---|
31
+ | `-f`, `--from-date` | Start date (YYYY-MM-DD) |
32
+ | `-t`, `--to-date` | End date (YYYY-MM-DD) |
33
+ | `-v`, `--fix-version` | Expected Jira fixVersion |
34
+ | `-r`, `--repo` | GitHub repo (owner/repo) |
35
+ | `-c`, `--config` | Config file (default: `mergescope.yaml`) |
36
+ | `--mcp-config` | Amazon Q MCP config path |
37
+ | `--llm-provider` | `anthropic`, `bedrock`, `copilot`, or `local` |
38
+ | `--llm-model` | Model ID |
39
+ | `--llm-base-url` | API base URL (copilot/local) |
40
+ | `--language` | Response language (e.g. Spanish) |
41
+ | `--verbose` | Debug logging |
42
+
43
+ ### Multilingual prompts
44
+
45
+ Set a custom prompt in any language via `mergescope.yaml`:
46
+
47
+ ```yaml
48
+ language: "es"
49
+ response_language: "Spanish"
50
+ prompt_template: |
51
+ Necesito auditar tickets mergeados entre {from_date} y {to_date}.
52
+ La fixVersion esperada es {fix_version}.
53
+ Extrae los IDs de JIRA desde los merge commits del repositorio {repo}
54
+ y valida su fixVersion.
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ Add a `mergescope.yaml` in your project root:
60
+
61
+ ```yaml
62
+ repo: "nice/java-project"
63
+ jira_project_prefix: "PROJ"
64
+ jira_base_url: "https://company.jira.com"
65
+
66
+ llm:
67
+ provider: "anthropic" # anthropic, bedrock, copilot, local
68
+ model: "claude-sonnet-4-6-20250620"
69
+
70
+ mcp:
71
+ servers:
72
+ github:
73
+ name: "github-mcp-server"
74
+ jira:
75
+ name: "atlassian-mcp-server"
76
+ ```
77
+
78
+ ### LLM providers
79
+
80
+ | Provider | Model example | Notes |
81
+ |---|---|---|
82
+ | `anthropic` | `claude-sonnet-4-6-20250620` | Uses `ANTHROPIC_API_KEY` |
83
+ | `bedrock` | `us.anthropic.claude-sonnet-4-20250514-v1:0` | Uses AWS credentials |
84
+ | `copilot` | `claude-sonnet-4` | Set `base_url` to Copilot endpoint |
85
+ | `local` | `qwen3-8b` | LM Studio, Ollama, or any OpenAI-compatible server |
86
+
87
+ ### MCP servers
88
+
89
+ Two modes — pick what fits your setup:
90
+
91
+ **With Amazon Q** (default): just set server names. MergeScope finds them in `~/.aws/amazonq/mcp.json` or `.amazonq/mcp.json`.
92
+
93
+ **Standalone** (no Amazon Q): define `command`, `args`, and `env` directly:
94
+
95
+ ```yaml
96
+ mcp:
97
+ servers:
98
+ github:
99
+ name: "github-mcp-server"
100
+ command: "npx"
101
+ args: ["-y", "@anthropic/github-mcp-server"]
102
+ env:
103
+ GITHUB_TOKEN: "ghp_..."
104
+ jira:
105
+ name: "atlassian-mcp-server"
106
+ command: "npx"
107
+ args: ["-y", "@anthropic/atlassian-mcp-server"]
108
+ env:
109
+ JIRA_API_TOKEN: "..."
110
+ JIRA_URL: "https://company.jira.com"
111
+ ```
112
+
113
+ You can mix both — some servers inline, others from Amazon Q.
114
+
115
+ ### Environment variables
116
+
117
+ `MERGESCOPE_REPO`, `MERGESCOPE_LLM_PROVIDER`, `MERGESCOPE_LLM_MODEL`, `MERGESCOPE_LLM_BASE_URL`, `MERGESCOPE_LLM_API_KEY`, `MERGESCOPE_JIRA_BASE_URL`, `MERGESCOPE_LANGUAGE`
118
+
119
+ ## Requirements
120
+
121
+ - Python 3.11+
122
+ - GitHub and Jira MCP servers (standalone or via Amazon Q)
123
+ - An LLM: Anthropic API key, AWS credentials, GitHub Copilot, or a local server
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ git clone https://github.com/martin5211/MergeScope.git
129
+ cd MergeScope
130
+ uv venv && uv pip install -e .
131
+ pytest
132
+ ```
@@ -0,0 +1,81 @@
1
+ # MergeScope configuration
2
+ # Copy this file to your project root and adjust values.
3
+
4
+ # GitHub repository (owner/repo)
5
+ repo: ""
6
+
7
+ # Jira settings
8
+ jira_project_prefix: "" # e.g. "PROJ" — leave empty to match all projects
9
+ jira_base_url: "" # e.g. "https://company.jira.com"
10
+
11
+ # Language for the audit prompt and report
12
+ language: "en"
13
+ response_language: "English"
14
+
15
+ # Custom prompt template (supports {repo}, {from_date}, {to_date}, {fix_version})
16
+ # prompt_template: |
17
+ # Necesito auditar tickets mergeados entre {from_date} y {to_date}.
18
+ # La fixVersion esperada es {fix_version}.
19
+ # Extrae los IDs de JIRA desde los merge commits del repositorio {repo}
20
+ # y valida su fixVersion.
21
+
22
+ # LLM provider and model
23
+ # Supported providers: anthropic, bedrock, copilot, local
24
+ llm:
25
+ provider: "anthropic"
26
+ model: "claude-sonnet-4-6-20250620"
27
+ # base_url: "" # override API endpoint (required for copilot/local)
28
+ # api_key: "" # override API key (or use env vars below)
29
+
30
+ # Provider examples:
31
+ #
32
+ # Anthropic (default):
33
+ # provider: "anthropic"
34
+ # model: "claude-sonnet-4-6-20250620"
35
+ # # uses ANTHROPIC_API_KEY env var
36
+ #
37
+ # AWS Bedrock:
38
+ # provider: "bedrock"
39
+ # model: "us.anthropic.claude-sonnet-4-20250514-v1:0"
40
+ # # uses AWS credentials
41
+ #
42
+ # GitHub Copilot:
43
+ # provider: "copilot"
44
+ # model: "claude-sonnet-4"
45
+ # base_url: "https://api.githubcopilot.com"
46
+ # # uses GITHUB_TOKEN or MERGESCOPE_LLM_API_KEY env var
47
+ #
48
+ # Local LLM (LM Studio, Ollama, etc.):
49
+ # provider: "local"
50
+ # model: "qwen3-9b"
51
+ # base_url: "http://localhost:1234/v1"
52
+
53
+ # MCP server configuration
54
+ # Two modes: inline (standalone) or Amazon Q lookup (name-only).
55
+ # When "command" is set, the server runs directly — no Amazon Q needed.
56
+ # When only "name" is set, it looks up that server in Amazon Q config.
57
+ mcp:
58
+ config_path: "" # auto-discovers Amazon Q config if empty
59
+ servers:
60
+ github:
61
+ name: "github-mcp-server"
62
+ jira:
63
+ name: "atlassian-mcp-server"
64
+
65
+ # Standalone example (no Amazon Q required):
66
+ #
67
+ # mcp:
68
+ # servers:
69
+ # github:
70
+ # name: "github-mcp-server"
71
+ # command: "npx"
72
+ # args: ["-y", "@anthropic/github-mcp-server"]
73
+ # env:
74
+ # GITHUB_TOKEN: "ghp_..."
75
+ # jira:
76
+ # name: "atlassian-mcp-server"
77
+ # command: "npx"
78
+ # args: ["-y", "@anthropic/atlassian-mcp-server"]
79
+ # env:
80
+ # JIRA_API_TOKEN: "..."
81
+ # JIRA_URL: "https://company.atlassian.net"
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mergescope"
7
+ version = "0.1.0"
8
+ description = "Audit merged PRs against Jira tickets using a LangGraph agent and MCP tools"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Martin Caminoa" }]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Software Development :: Quality Assurance",
23
+ ]
24
+ dependencies = [
25
+ "langgraph>=0.4,<2.0",
26
+ "langchain-mcp-adapters>=0.1,<2.0",
27
+ "langchain-anthropic>=0.3,<2.0",
28
+ "langchain-aws>=0.2,<2.0",
29
+ "langchain-openai>=0.3,<2.0",
30
+ "pyyaml>=6.0,<7.0",
31
+ "rich>=13.0,<15.0",
32
+ ]
33
+
34
+ [project.scripts]
35
+ mergescope = "mergescope.cli:main"
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/martin5211/MergeScope"
39
+ Repository = "https://github.com/martin5211/MergeScope"
40
+ Issues = "https://github.com/martin5211/MergeScope/issues"
41
+
42
+ [dependency-groups]
43
+ dev = ["pytest>=9.0"]
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/mergescope"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from mergescope.cli import main
2
+
3
+ main()
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import re
6
+ from typing import Any
7
+
8
+ from langchain_core.messages import HumanMessage, SystemMessage
9
+ from langgraph.graph import MessagesState, StateGraph, START
10
+ from langgraph.prebuilt import ToolNode, tools_condition
11
+
12
+ from mergescope.config import MergeScopeConfig
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ SYSTEM_PROMPT = """\
17
+ You are MergeScope, an audit agent that cross-references GitHub merge activity with Jira tickets.
18
+
19
+ You have access to GitHub and Jira tools via MCP servers. Use the available tools to complete the audit.
20
+
21
+ ## Instructions
22
+
23
+ 1. **Fetch merge activity**: Use the GitHub tools to list merged pull requests (or merge commits) in the given repository and date range. Get enough detail: PR title, body/description, branch name, commit messages, and commit SHAs.
24
+
25
+ 2. **Extract Jira IDs**: From EACH pull request, look for Jira ticket IDs in ALL of these places:
26
+ - PR title
27
+ - PR body / description (both short and long)
28
+ - Branch name (e.g. `feat/PROJ-123-something`, `PROJ-456`, `bugfix/proj-789`)
29
+ - Individual commit messages
30
+ Jira IDs follow the pattern: 2+ letters, a hyphen, then digits (e.g. PROJ-123, AB-1). They may appear in brackets [PROJ-123], lowercase, or mixed case. Normalize all to UPPERCASE and deduplicate.
31
+
32
+ 3. **Validate in Jira**: For each unique Jira ID found, use the Jira/Atlassian tools to fetch:
33
+ - The ticket summary (title)
34
+ - The fixVersion field
35
+ Compare fixVersion against the expected version provided.
36
+
37
+ 4. **Return structured JSON** with this exact schema:
38
+ ```json
39
+ {{
40
+ "tickets": [
41
+ {{
42
+ "jira_id": "PROJ-123",
43
+ "title": "Ticket summary from Jira",
44
+ "fix_version": "actual fixVersion or null",
45
+ "expected_version": "the expected version",
46
+ "status": "MATCH | MISMATCH | NOT_FOUND | NO_FIX_VERSION"
47
+ }}
48
+ ],
49
+ "unlinked_commits": [
50
+ {{
51
+ "sha": "full commit SHA",
52
+ "message": "first line of commit message",
53
+ "url": "https://github.com/owner/repo/commit/SHA"
54
+ }}
55
+ ]
56
+ }}
57
+ ```
58
+
59
+ Status rules:
60
+ - `MATCH`: fixVersion equals expected version
61
+ - `MISMATCH`: fixVersion exists but differs from expected
62
+ - `NOT_FOUND`: Jira ticket does not exist or lookup failed
63
+ - `NO_FIX_VERSION`: ticket exists but has no fixVersion set
64
+
65
+ Include in `unlinked_commits` every merge commit or squash commit that has NO Jira ID anywhere (title, body, branch, message).
66
+
67
+ {language_instruction}
68
+
69
+ Respond ONLY with the JSON object. No markdown fences, no explanation.\
70
+ """
71
+
72
+
73
+ def _build_system_prompt(config: MergeScopeConfig) -> str:
74
+ lang = config.response_language
75
+ if lang and lang.lower() != "english":
76
+ instruction = f"Write all ticket titles and the JSON string values in {lang} if the source is in that language, otherwise keep the original language."
77
+ else:
78
+ instruction = ""
79
+ return SYSTEM_PROMPT.format(language_instruction=instruction)
80
+
81
+
82
+ _PROVIDERS = ("anthropic", "bedrock", "copilot", "local")
83
+
84
+
85
+ def _create_llm(config: MergeScopeConfig):
86
+ provider = config.llm.provider.lower()
87
+ model_id = config.llm.model
88
+ base_url = config.llm.base_url or None
89
+ api_key = config.llm.api_key or None
90
+
91
+ if provider == "anthropic":
92
+ from langchain_anthropic import ChatAnthropic
93
+ return ChatAnthropic(model=model_id)
94
+
95
+ if provider == "bedrock":
96
+ from langchain_aws import ChatBedrock
97
+ return ChatBedrock(model_id=model_id)
98
+
99
+ if provider == "copilot":
100
+ from langchain_openai import ChatOpenAI
101
+ return ChatOpenAI(
102
+ model=model_id,
103
+ base_url=base_url or "https://api.githubcopilot.com",
104
+ api_key=api_key or None,
105
+ )
106
+
107
+ if provider == "local":
108
+ from langchain_openai import ChatOpenAI
109
+ return ChatOpenAI(
110
+ model=model_id,
111
+ base_url=base_url or "http://localhost:1234/v1",
112
+ api_key=api_key or "lm-studio",
113
+ )
114
+
115
+ raise ValueError(
116
+ f"Unsupported LLM provider: '{provider}'. "
117
+ f"Choose from: {', '.join(_PROVIDERS)}"
118
+ )
119
+
120
+
121
+ def build_agent(tools: list, config: MergeScopeConfig):
122
+ model = _create_llm(config)
123
+ model_with_tools = model.bind_tools(tools)
124
+
125
+ async def call_model(state: MessagesState) -> dict:
126
+ response = await model_with_tools.ainvoke(state["messages"])
127
+ return {"messages": [response]}
128
+
129
+ graph = StateGraph(MessagesState)
130
+ graph.add_node("call_model", call_model)
131
+ graph.add_node("tools", ToolNode(tools))
132
+ graph.add_edge(START, "call_model")
133
+ graph.add_conditional_edges("call_model", tools_condition)
134
+ graph.add_edge("tools", "call_model")
135
+
136
+ return graph.compile()
137
+
138
+
139
+ def build_prompt(config: MergeScopeConfig, repo: str, from_date: str, to_date: str, fix_version: str) -> list:
140
+ system = _build_system_prompt(config)
141
+ human = config.prompt_template.format(
142
+ repo=repo,
143
+ from_date=from_date,
144
+ to_date=to_date,
145
+ fix_version=fix_version,
146
+ )
147
+ return [
148
+ SystemMessage(content=system),
149
+ HumanMessage(content=human),
150
+ ]
151
+
152
+
153
+ def parse_agent_output(content: str) -> dict[str, Any]:
154
+ text = content.strip()
155
+ fence_match = re.search(r"```(?:json)?\s*\n?(.*?)\n?\s*```", text, re.DOTALL)
156
+ if fence_match:
157
+ text = fence_match.group(1).strip()
158
+
159
+ try:
160
+ return json.loads(text)
161
+ except json.JSONDecodeError as exc:
162
+ logger.error("Failed to parse agent output as JSON: %s", exc)
163
+ logger.debug("Raw output:\n%s", content)
164
+ raise ValueError(
165
+ "Agent did not return valid JSON. Raw output logged at DEBUG level."
166
+ ) from exc