Skip to content

feat: Add GitHub integration with agent_prompts and github_components #1637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 53 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4aba2a9
add agent_prompts and github_components
julian-risch Apr 10, 2025
890ecde
rename to github_haystack
julian-risch Apr 10, 2025
b99ad72
remove github-haystack
julian-risch Apr 10, 2025
97029b1
Merge branch 'main' into move-github-components
julian-risch Apr 15, 2025
af37ecd
renamed integration, added components dir
julian-risch Apr 15, 2025
ac4bf31
add tests, pydoc, update pyproject.toml
julian-risch Apr 16, 2025
2e2202a
add workflow
julian-risch Apr 16, 2025
fcfec47
fmt
julian-risch Apr 16, 2025
79dc7db
fmt
julian-risch Apr 16, 2025
cf3a2f5
lint
julian-risch Apr 16, 2025
f82412f
ruff
julian-risch Apr 16, 2025
fa9cbe3
fmt
julian-risch Apr 16, 2025
c4beece
lint:all
julian-risch Apr 16, 2025
4a6b81b
replace StrEnum for py 3.9+ compatibility
julian-risch Apr 16, 2025
e77a49e
move files
julian-risch Apr 24, 2025
4e49081
fix tests
julian-risch Apr 24, 2025
7144176
lint
julian-risch Apr 24, 2025
b4a375d
fix pydoc and extend init files
julian-risch Apr 25, 2025
2b8bc14
Add integration:github to labeler.yml
julian-risch Apr 25, 2025
8480832
unify how we set GITHUB_TOKEN in tests
julian-risch Apr 25, 2025
ca977a1
fix 3 usage examples. 3 remaining
julian-risch Apr 25, 2025
b735b76
Merge branch 'main' into move-github-components
julian-risch Apr 25, 2025
e5111ed
remove empty lines from prompts
julian-risch Apr 30, 2025
dc08916
GitHub capitalization
julian-risch Apr 30, 2025
380c212
add license header
julian-risch Apr 30, 2025
ea95934
all caps for prompts
julian-risch Apr 30, 2025
22758e8
add GitHubFileEditorTool
julian-risch May 2, 2025
0c4f0f0
enforce kwargs instead of positional args
julian-risch May 2, 2025
31add35
use _get_request_headers and base_headers consistently
julian-risch May 5, 2025
474674c
lint
julian-risch May 5, 2025
b30b21f
rename GitHubRepositoryViewer to GitHubRepoViewer
julian-risch May 5, 2025
4f8d0d3
lint
julian-risch May 5, 2025
4555d2a
add pipeline serialization test
julian-risch May 5, 2025
8f4bf9f
extend pipeline to_dict test
julian-risch May 6, 2025
ec64b09
set default branch of repo viewer
julian-risch May 6, 2025
9ae7857
lint
julian-risch May 6, 2025
d0480c2
add four more tools
julian-risch May 6, 2025
ba9b1ea
lint
julian-risch May 6, 2025
eb7a267
rename prompts
julian-risch May 7, 2025
d98d3ec
add tests for four more tools
julian-risch May 7, 2025
b8bb2ed
rename context prompt
julian-risch May 27, 2025
8635fa5
add outputs_to_state as param to GitHubFileEditorTool
julian-risch May 27, 2025
91ac951
add outputs_to_state as param to GitHubRepoViewerTool
julian-risch May 27, 2025
560abfd
set default outputs_to_state for GitHubRepoViewerTool
julian-risch May 27, 2025
d754b01
extract serialize_handlers to utils; don't use mutable defaults
julian-risch May 27, 2025
9860266
replace init_parameters with data for serde in FileEditor, RepoViewer
julian-risch May 27, 2025
0c52ff4
add outputs_to_state to GitHubIssueCommenterTool; replace init_parame…
julian-risch May 27, 2025
3160373
add outputs_to_state to GitHubIssueViewerTool; replace init_parameter…
julian-risch May 27, 2025
f088c18
add outputs_to_state to GitHubPRCreatorTool; replace init_parameters …
julian-risch May 27, 2025
a822bc1
move param docstrings to init methods
julian-risch May 28, 2025
f911175
use generate_qualified_class_name instead of hardcoded name
julian-risch May 28, 2025
1ac5ba2
test with lowest supported version
julian-risch May 28, 2025
fc027f5
don't test http_client_kwargs for compatibility with Haystack 2.12
julian-risch May 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,46 @@
# SPDX-FileCopyrightText: 2023-present deepset GmbH <[email protected]>
#
# SPDX-License-Identifier: Apache-2.0
from typing import Any, Dict, Optional
from typing import Any, Callable, Dict, Optional, Union

from haystack import default_from_dict, default_to_dict
from haystack.tools import ComponentTool
from haystack.utils import Secret, deserialize_secrets_inplace

from haystack_integrations.components.connectors.github.issue_commenter import GitHubIssueCommenter
from haystack_integrations.prompts.github.issue_commenter_prompt import ISSUE_COMMENTER_PROMPT, ISSUE_COMMENTER_SCHEMA
from haystack_integrations.tools.github.utils import deserialize_handlers, serialize_handlers


class GitHubIssueCommenterTool(ComponentTool):
"""
A tool for commenting on GitHub issues.

:param name: Optional name for the tool.
:param description: Optional description.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do realize this is a bit confusing in our tools, but it seems that if we define a __init__ then these docstrings are put under the __init__ def. If there is no __init__ defined like in Tool then we put it in the class description.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, of course! Do you think we should a usage example here then (in addition to moving the param docstrings to the init)? I realized that's missing too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah if it's not too much to ask, a usage example would be great!

:param parameters: Optional JSON schema defining the parameters expected by the Tool.
:param github_token: GitHub personal access token for API authentication
:param raise_on_failure: If True, raises exceptions on API errors
:param retry_attempts: Number of retry attempts for failed requests
:param outputs_to_string:
Optional dictionary defining how a tool outputs should be converted into a string.
If the source is provided only the specified output key is sent to the handler.
If the source is omitted the whole tool result is sent to the handler.
Example: {
"source": "docs", "handler": format_documents
}
:param inputs_from_state:
Optional dictionary mapping state keys to tool parameter names.
Example: {"repository": "repo"} maps state's "repository" to tool's "repo" parameter.
:param outputs_to_state:
Optional dictionary defining how tool outputs map to keys within state as well as optional handlers.
If the source is provided only the specified output key is sent to the handler.
Example: {
"documents": {"source": "docs", "handler": custom_handler}
}
If the source is omitted the whole tool result is sent to the handler.
Example: {
"documents": {"handler": custom_handler}
}
"""

def __init__(
Expand All @@ -25,13 +52,19 @@ def __init__(
github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"),
raise_on_failure: bool = True,
retry_attempts: int = 2,
outputs_to_string: Optional[Dict[str, Union[str, Callable[[Any], str]]]] = None,
inputs_from_state: Optional[Dict[str, str]] = None,
outputs_to_state: Optional[Dict[str, Dict[str, Union[str, Callable]]]] = None,
):
self.name = name
self.description = description
self.parameters = parameters
self.github_token = github_token
self.raise_on_failure = raise_on_failure
self.retry_attempts = retry_attempts
self.outputs_to_string = outputs_to_string
self.inputs_from_state = inputs_from_state
self.outputs_to_state = outputs_to_state

issue_commenter = GitHubIssueCommenter(
github_token=github_token,
Expand All @@ -43,6 +76,9 @@ def __init__(
name=name,
description=description,
parameters=parameters,
outputs_to_string=outputs_to_string,
inputs_from_state=inputs_from_state,
outputs_to_state=outputs_to_state,
)

def to_dict(self) -> Dict[str, Any]:
Expand All @@ -52,15 +88,23 @@ def to_dict(self) -> Dict[str, Any]:
:returns:
Dictionary with serialized data.
"""
return default_to_dict(
self,
name=self.name,
description=self.description,
parameters=self.parameters,
github_token=self.github_token.to_dict() if self.github_token else None,
raise_on_failure=self.raise_on_failure,
retry_attempts=self.retry_attempts,
)
serialized = {
"name": self.name,
"description": self.description,
"parameters": self.parameters,
"github_token": self.github_token.to_dict() if self.github_token else None,
"raise_on_failure": self.raise_on_failure,
"retry_attempts": self.retry_attempts,
"outputs_to_string": self.outputs_to_string,
"inputs_from_state": self.inputs_from_state,
"outputs_to_state": self.outputs_to_state,
}

serialize_handlers(serialized, self.outputs_to_state, self.outputs_to_string)
return {
"type": "haystack_integrations.tools.github.issue_commenter_tool.GitHubIssueCommenterTool",
"data": serialized,
}

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "GitHubIssueCommenterTool":
Expand All @@ -72,5 +116,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "GitHubIssueCommenterTool":
:returns:
Deserialized tool.
"""
deserialize_secrets_inplace(data["init_parameters"], keys=["github_token"])
return default_from_dict(cls, data)
inner_data = data["data"]
deserialize_secrets_inplace(inner_data, keys=["github_token"])
deserialize_handlers(inner_data)
return cls(**inner_data)
92 changes: 85 additions & 7 deletions integrations/github/tests/test_issue_commenter_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from haystack_integrations.prompts.github.issue_commenter_prompt import ISSUE_COMMENTER_PROMPT, ISSUE_COMMENTER_SCHEMA
from haystack_integrations.tools.github.issue_commenter_tool import GitHubIssueCommenterTool
from haystack_integrations.tools.github.utils import message_handler


class TestGitHubIssueCommenterTool:
Expand All @@ -15,18 +16,24 @@ def test_init(self, monkeypatch):
assert tool.description == ISSUE_COMMENTER_PROMPT
assert tool.parameters == ISSUE_COMMENTER_SCHEMA
assert tool.retry_attempts == 2
assert tool.outputs_to_string is None
assert tool.inputs_from_state is None
assert tool.outputs_to_state is None

def test_from_dict(self, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
tool_dict = {
"type": "haystack_integrations.tools.github.issue_commenter_tool.GitHubIssueCommenterTool",
"init_parameters": {
"data": {
"name": "issue_commenter",
"description": ISSUE_COMMENTER_PROMPT,
"parameters": ISSUE_COMMENTER_SCHEMA,
"github_token": {"env_vars": ["GITHUB_TOKEN"], "strict": True, "type": "env_var"},
"raise_on_failure": True,
"retry_attempts": 2,
"outputs_to_string": None,
"inputs_from_state": None,
"outputs_to_state": None,
},
}
tool = GitHubIssueCommenterTool.from_dict(tool_dict)
Expand All @@ -36,19 +43,90 @@ def test_from_dict(self, monkeypatch):
assert tool.github_token == Secret.from_env_var("GITHUB_TOKEN")
assert tool.raise_on_failure
assert tool.retry_attempts == 2
assert tool.outputs_to_string is None
assert tool.inputs_from_state is None
assert tool.outputs_to_state is None

def test_to_dict(self, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
tool = GitHubIssueCommenterTool()
tool_dict = tool.to_dict()
assert tool_dict["type"] == "haystack_integrations.tools.github.issue_commenter_tool.GitHubIssueCommenterTool"
assert tool_dict["init_parameters"]["name"] == "issue_commenter"
assert tool_dict["init_parameters"]["description"] == ISSUE_COMMENTER_PROMPT
assert tool_dict["init_parameters"]["parameters"] == ISSUE_COMMENTER_SCHEMA
assert tool_dict["init_parameters"]["github_token"] == {
assert tool_dict["data"]["name"] == "issue_commenter"
assert tool_dict["data"]["description"] == ISSUE_COMMENTER_PROMPT
assert tool_dict["data"]["parameters"] == ISSUE_COMMENTER_SCHEMA
assert tool_dict["data"]["github_token"] == {
"env_vars": ["GITHUB_TOKEN"],
"strict": True,
"type": "env_var",
}
assert tool_dict["init_parameters"]["raise_on_failure"]
assert tool_dict["init_parameters"]["retry_attempts"] == 2
assert tool_dict["data"]["raise_on_failure"]
assert tool_dict["data"]["retry_attempts"] == 2
assert tool_dict["data"]["outputs_to_string"] is None
assert tool_dict["data"]["inputs_from_state"] is None
assert tool_dict["data"]["outputs_to_state"] is None

def test_to_dict_with_extra_params(self, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
tool = GitHubIssueCommenterTool(
name="test_issue_commenter",
description="Test description",
parameters={"type": "object", "properties": {}},
github_token=None,
raise_on_failure=False,
retry_attempts=3,
outputs_to_string={"handler": message_handler},
inputs_from_state={"repository": "repo"},
outputs_to_state={"documents": {"source": "docs", "handler": message_handler}},
)
tool_dict = tool.to_dict()
assert tool_dict["type"] == "haystack_integrations.tools.github.issue_commenter_tool.GitHubIssueCommenterTool"
assert tool_dict["data"]["name"] == "test_issue_commenter"
assert tool_dict["data"]["description"] == "Test description"
assert tool_dict["data"]["parameters"] == {"type": "object", "properties": {}}
assert tool_dict["data"]["github_token"] is None
assert tool_dict["data"]["raise_on_failure"] is False
assert tool_dict["data"]["retry_attempts"] == 3
assert (
tool_dict["data"]["outputs_to_string"]["handler"]
== "haystack_integrations.tools.github.utils.message_handler"
)
assert tool_dict["data"]["inputs_from_state"] == {"repository": "repo"}
assert tool_dict["data"]["outputs_to_state"]["documents"]["source"] == "docs"
assert (
tool_dict["data"]["outputs_to_state"]["documents"]["handler"]
== "haystack_integrations.tools.github.utils.message_handler"
)

def test_from_dict_with_extra_params(self, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
tool_dict = {
"type": "haystack_integrations.tools.github.issue_commenter_tool.GitHubIssueCommenterTool",
"data": {
"name": "test_issue_commenter",
"description": "Test description",
"parameters": {"type": "object", "properties": {}},
"github_token": None,
"raise_on_failure": False,
"retry_attempts": 3,
"outputs_to_string": {"handler": "haystack_integrations.tools.github.utils.message_handler"},
"inputs_from_state": {"repository": "repo"},
"outputs_to_state": {
"documents": {
"source": "docs",
"handler": "haystack_integrations.tools.github.utils.message_handler",
}
},
},
}
tool = GitHubIssueCommenterTool.from_dict(tool_dict)
assert tool.name == "test_issue_commenter"
assert tool.description == "Test description"
assert tool.parameters == {"type": "object", "properties": {}}
assert tool.github_token is None
assert tool.raise_on_failure is False
assert tool.retry_attempts == 3
assert tool.outputs_to_string["handler"] == message_handler
assert tool.inputs_from_state == {"repository": "repo"}
assert tool.outputs_to_state["documents"]["source"] == "docs"
assert tool.outputs_to_state["documents"]["handler"] == message_handler