Compare commits

...

3 Commits

Author SHA1 Message Date
Zamil Majdy
7474cf355a feat(blocks): add plural outputs to blocks yielding singular values in loops
Add missing plural output versions to blocks that yield individual items
in loops but don't provide the complete collection:

- GetRedditPostsBlock: add `posts` output for all Reddit posts
- ReadRSSFeedBlock: add `entries` output for all RSS entries
- AddMemoryBlock: add `results` output for all memory operation results

This allows users to access both individual items (for iteration) and the
complete collection (for aggregate operations) from the same block execution.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 12:52:33 -07:00
Zamil Majdy
05d4cd3c6c Update autogpt_platform/backend/backend/blocks/github/pull_requests.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-04 02:38:48 +07:00
Toran Bruce Richards
8f317db046 feat(platform/backend): yield full lists in GitHub blocks 2025-07-03 20:31:00 +01:00
6 changed files with 180 additions and 26 deletions

View File

@@ -498,6 +498,9 @@ class GithubListIssuesBlock(Block):
issue: IssueItem = SchemaField( issue: IssueItem = SchemaField(
title="Issue", description="Issues with their title and URL" title="Issue", description="Issues with their title and URL"
) )
issues: list[IssueItem] = SchemaField(
description="List of issues with their title and URL"
)
error: str = SchemaField(description="Error message if listing issues failed") error: str = SchemaField(description="Error message if listing issues failed")
def __init__(self): def __init__(self):
@@ -519,7 +522,16 @@ class GithubListIssuesBlock(Block):
"title": "Issue 1", "title": "Issue 1",
"url": "https://github.com/owner/repo/issues/1", "url": "https://github.com/owner/repo/issues/1",
}, },
) ),
(
"issues",
[
{
"title": "Issue 1",
"url": "https://github.com/owner/repo/issues/1",
}
],
),
], ],
test_mock={ test_mock={
"list_issues": lambda *args, **kwargs: [ "list_issues": lambda *args, **kwargs: [
@@ -551,10 +563,12 @@ class GithubListIssuesBlock(Block):
credentials: GithubCredentials, credentials: GithubCredentials,
**kwargs, **kwargs,
) -> BlockOutput: ) -> BlockOutput:
for issue in await self.list_issues( issues = await self.list_issues(
credentials, credentials,
input_data.repo_url, input_data.repo_url,
): )
yield "issues", issues
for issue in issues:
yield "issue", issue yield "issue", issue

View File

@@ -31,7 +31,10 @@ class GithubListPullRequestsBlock(Block):
pull_request: PRItem = SchemaField( pull_request: PRItem = SchemaField(
title="Pull Request", description="PRs with their title and URL" title="Pull Request", description="PRs with their title and URL"
) )
error: str = SchemaField(description="Error message if listing issues failed") pull_requests: list[PRItem] = SchemaField(
description="List of pull requests with their title and URL"
)
error: str = SchemaField(description="Error message if listing pull requests failed")
def __init__(self): def __init__(self):
super().__init__( super().__init__(
@@ -52,7 +55,16 @@ class GithubListPullRequestsBlock(Block):
"title": "Pull request 1", "title": "Pull request 1",
"url": "https://github.com/owner/repo/pull/1", "url": "https://github.com/owner/repo/pull/1",
}, },
) ),
(
"pull_requests",
[
{
"title": "Pull request 1",
"url": "https://github.com/owner/repo/pull/1",
}
],
),
], ],
test_mock={ test_mock={
"list_prs": lambda *args, **kwargs: [ "list_prs": lambda *args, **kwargs: [
@@ -88,6 +100,7 @@ class GithubListPullRequestsBlock(Block):
credentials, credentials,
input_data.repo_url, input_data.repo_url,
) )
yield "pull_requests", pull_requests
for pr in pull_requests: for pr in pull_requests:
yield "pull_request", pr yield "pull_request", pr
@@ -460,6 +473,9 @@ class GithubListPRReviewersBlock(Block):
title="Reviewer", title="Reviewer",
description="Reviewers with their username and profile URL", description="Reviewers with their username and profile URL",
) )
reviewers: list[ReviewerItem] = SchemaField(
description="List of reviewers with their username and profile URL"
)
error: str = SchemaField( error: str = SchemaField(
description="Error message if listing reviewers failed" description="Error message if listing reviewers failed"
) )
@@ -483,7 +499,16 @@ class GithubListPRReviewersBlock(Block):
"username": "reviewer1", "username": "reviewer1",
"url": "https://github.com/reviewer1", "url": "https://github.com/reviewer1",
}, },
) ),
(
"reviewers",
[
{
"username": "reviewer1",
"url": "https://github.com/reviewer1",
}
],
),
], ],
test_mock={ test_mock={
"list_reviewers": lambda *args, **kwargs: [ "list_reviewers": lambda *args, **kwargs: [
@@ -516,10 +541,12 @@ class GithubListPRReviewersBlock(Block):
credentials: GithubCredentials, credentials: GithubCredentials,
**kwargs, **kwargs,
) -> BlockOutput: ) -> BlockOutput:
for reviewer in await self.list_reviewers( reviewers = await self.list_reviewers(
credentials, credentials,
input_data.pr_url, input_data.pr_url,
): )
yield "reviewers", reviewers
for reviewer in reviewers:
yield "reviewer", reviewer yield "reviewer", reviewer

View File

@@ -31,6 +31,9 @@ class GithubListTagsBlock(Block):
tag: TagItem = SchemaField( tag: TagItem = SchemaField(
title="Tag", description="Tags with their name and file tree browser URL" title="Tag", description="Tags with their name and file tree browser URL"
) )
tags: list[TagItem] = SchemaField(
description="List of tags with their name and file tree browser URL"
)
error: str = SchemaField(description="Error message if listing tags failed") error: str = SchemaField(description="Error message if listing tags failed")
def __init__(self): def __init__(self):
@@ -52,7 +55,16 @@ class GithubListTagsBlock(Block):
"name": "v1.0.0", "name": "v1.0.0",
"url": "https://github.com/owner/repo/tree/v1.0.0", "url": "https://github.com/owner/repo/tree/v1.0.0",
}, },
) ),
(
"tags",
[
{
"name": "v1.0.0",
"url": "https://github.com/owner/repo/tree/v1.0.0",
}
],
),
], ],
test_mock={ test_mock={
"list_tags": lambda *args, **kwargs: [ "list_tags": lambda *args, **kwargs: [
@@ -93,6 +105,7 @@ class GithubListTagsBlock(Block):
credentials, credentials,
input_data.repo_url, input_data.repo_url,
) )
yield "tags", tags
for tag in tags: for tag in tags:
yield "tag", tag yield "tag", tag
@@ -114,6 +127,9 @@ class GithubListBranchesBlock(Block):
title="Branch", title="Branch",
description="Branches with their name and file tree browser URL", description="Branches with their name and file tree browser URL",
) )
branches: list[BranchItem] = SchemaField(
description="List of branches with their name and file tree browser URL"
)
error: str = SchemaField(description="Error message if listing branches failed") error: str = SchemaField(description="Error message if listing branches failed")
def __init__(self): def __init__(self):
@@ -135,7 +151,16 @@ class GithubListBranchesBlock(Block):
"name": "main", "name": "main",
"url": "https://github.com/owner/repo/tree/main", "url": "https://github.com/owner/repo/tree/main",
}, },
) ),
(
"branches",
[
{
"name": "main",
"url": "https://github.com/owner/repo/tree/main",
}
],
),
], ],
test_mock={ test_mock={
"list_branches": lambda *args, **kwargs: [ "list_branches": lambda *args, **kwargs: [
@@ -176,6 +201,7 @@ class GithubListBranchesBlock(Block):
credentials, credentials,
input_data.repo_url, input_data.repo_url,
) )
yield "branches", branches
for branch in branches: for branch in branches:
yield "branch", branch yield "branch", branch
@@ -199,6 +225,9 @@ class GithubListDiscussionsBlock(Block):
discussion: DiscussionItem = SchemaField( discussion: DiscussionItem = SchemaField(
title="Discussion", description="Discussions with their title and URL" title="Discussion", description="Discussions with their title and URL"
) )
discussions: list[DiscussionItem] = SchemaField(
description="List of discussions with their title and URL"
)
error: str = SchemaField( error: str = SchemaField(
description="Error message if listing discussions failed" description="Error message if listing discussions failed"
) )
@@ -223,7 +252,16 @@ class GithubListDiscussionsBlock(Block):
"title": "Discussion 1", "title": "Discussion 1",
"url": "https://github.com/owner/repo/discussions/1", "url": "https://github.com/owner/repo/discussions/1",
}, },
) ),
(
"discussions",
[
{
"title": "Discussion 1",
"url": "https://github.com/owner/repo/discussions/1",
}
],
),
], ],
test_mock={ test_mock={
"list_discussions": lambda *args, **kwargs: [ "list_discussions": lambda *args, **kwargs: [
@@ -279,6 +317,7 @@ class GithubListDiscussionsBlock(Block):
input_data.repo_url, input_data.repo_url,
input_data.num_discussions, input_data.num_discussions,
) )
yield "discussions", discussions
for discussion in discussions: for discussion in discussions:
yield "discussion", discussion yield "discussion", discussion
@@ -300,6 +339,9 @@ class GithubListReleasesBlock(Block):
title="Release", title="Release",
description="Releases with their name and file tree browser URL", description="Releases with their name and file tree browser URL",
) )
releases: list[ReleaseItem] = SchemaField(
description="List of releases with their name and file tree browser URL"
)
error: str = SchemaField(description="Error message if listing releases failed") error: str = SchemaField(description="Error message if listing releases failed")
def __init__(self): def __init__(self):
@@ -321,7 +363,16 @@ class GithubListReleasesBlock(Block):
"name": "v1.0.0", "name": "v1.0.0",
"url": "https://github.com/owner/repo/releases/tag/v1.0.0", "url": "https://github.com/owner/repo/releases/tag/v1.0.0",
}, },
) ),
(
"releases",
[
{
"name": "v1.0.0",
"url": "https://github.com/owner/repo/releases/tag/v1.0.0",
}
],
),
], ],
test_mock={ test_mock={
"list_releases": lambda *args, **kwargs: [ "list_releases": lambda *args, **kwargs: [
@@ -357,6 +408,7 @@ class GithubListReleasesBlock(Block):
credentials, credentials,
input_data.repo_url, input_data.repo_url,
) )
yield "releases", releases
for release in releases: for release in releases:
yield "release", release yield "release", release
@@ -1041,6 +1093,9 @@ class GithubListStargazersBlock(Block):
title="Stargazer", title="Stargazer",
description="Stargazers with their username and profile URL", description="Stargazers with their username and profile URL",
) )
stargazers: list[StargazerItem] = SchemaField(
description="List of stargazers with their username and profile URL"
)
error: str = SchemaField( error: str = SchemaField(
description="Error message if listing stargazers failed" description="Error message if listing stargazers failed"
) )
@@ -1064,7 +1119,16 @@ class GithubListStargazersBlock(Block):
"username": "octocat", "username": "octocat",
"url": "https://github.com/octocat", "url": "https://github.com/octocat",
}, },
) ),
(
"stargazers",
[
{
"username": "octocat",
"url": "https://github.com/octocat",
}
],
),
], ],
test_mock={ test_mock={
"list_stargazers": lambda *args, **kwargs: [ "list_stargazers": lambda *args, **kwargs: [
@@ -1104,5 +1168,6 @@ class GithubListStargazersBlock(Block):
credentials, credentials,
input_data.repo_url, input_data.repo_url,
) )
yield "stargazers", stargazers
for stargazer in stargazers: for stargazer in stargazers:
yield "stargazer", stargazer yield "stargazer", stargazer

View File

@@ -77,6 +77,9 @@ class AddMemoryBlock(Block, Mem0Base):
class Output(BlockSchema): class Output(BlockSchema):
action: str = SchemaField(description="Action of the operation") action: str = SchemaField(description="Action of the operation")
memory: str = SchemaField(description="Memory created") memory: str = SchemaField(description="Memory created")
results: list[dict[str, str]] = SchemaField(
description="List of all results from the operation"
)
error: str = SchemaField(description="Error message if operation fails") error: str = SchemaField(description="Error message if operation fails")
def __init__(self): def __init__(self):
@@ -104,8 +107,10 @@ class AddMemoryBlock(Block, Mem0Base):
}, },
], ],
test_output=[ test_output=[
("results", [{"event": "CREATED", "memory": "test memory"}]),
("action", "CREATED"), ("action", "CREATED"),
("memory", "test memory"), ("memory", "test memory"),
("results", [{"event": "CREATED", "memory": "test memory"}]),
("action", "CREATED"), ("action", "CREATED"),
("memory", "test memory"), ("memory", "test memory"),
], ],
@@ -150,8 +155,11 @@ class AddMemoryBlock(Block, Mem0Base):
**params, **params,
) )
if len(result.get("results", [])) > 0: results = result.get("results", [])
for result in result.get("results", []): yield "results", results
if len(results) > 0:
for result in results:
yield "action", result["event"] yield "action", result["event"]
yield "memory", result["memory"] yield "memory", result["memory"]
else: else:

View File

@@ -96,6 +96,7 @@ class GetRedditPostsBlock(Block):
class Output(BlockSchema): class Output(BlockSchema):
post: RedditPost = SchemaField(description="Reddit post") post: RedditPost = SchemaField(description="Reddit post")
posts: list[RedditPost] = SchemaField(description="List of all Reddit posts")
def __init__(self): def __init__(self):
super().__init__( super().__init__(
@@ -116,6 +117,23 @@ class GetRedditPostsBlock(Block):
"post_limit": 2, "post_limit": 2,
}, },
test_output=[ test_output=[
(
"posts",
[
RedditPost(
id="id1",
subreddit="subreddit",
title="title1",
body="body1",
),
RedditPost(
id="id2",
subreddit="subreddit",
title="title2",
body="body2",
),
],
),
( (
"post", "post",
RedditPost( RedditPost(
@@ -150,6 +168,7 @@ class GetRedditPostsBlock(Block):
self, input_data: Input, *, credentials: RedditCredentials, **kwargs self, input_data: Input, *, credentials: RedditCredentials, **kwargs
) -> BlockOutput: ) -> BlockOutput:
current_time = datetime.now(tz=timezone.utc) current_time = datetime.now(tz=timezone.utc)
all_posts = []
for post in self.get_posts(input_data=input_data, credentials=credentials): for post in self.get_posts(input_data=input_data, credentials=credentials):
if input_data.last_minutes: if input_data.last_minutes:
post_datetime = datetime.fromtimestamp( post_datetime = datetime.fromtimestamp(
@@ -162,12 +181,16 @@ class GetRedditPostsBlock(Block):
if input_data.last_post and post.id == input_data.last_post: if input_data.last_post and post.id == input_data.last_post:
break break
yield "post", RedditPost( reddit_post = RedditPost(
id=post.id, id=post.id,
subreddit=input_data.subreddit, subreddit=input_data.subreddit,
title=post.title, title=post.title,
body=post.selftext, body=post.selftext,
) )
all_posts.append(reddit_post)
yield "post", reddit_post
yield "posts", all_posts
class PostRedditCommentBlock(Block): class PostRedditCommentBlock(Block):

View File

@@ -40,6 +40,7 @@ class ReadRSSFeedBlock(Block):
class Output(BlockSchema): class Output(BlockSchema):
entry: RSSEntry = SchemaField(description="The RSS item") entry: RSSEntry = SchemaField(description="The RSS item")
entries: list[RSSEntry] = SchemaField(description="List of all RSS entries")
def __init__(self): def __init__(self):
super().__init__( super().__init__(
@@ -55,6 +56,21 @@ class ReadRSSFeedBlock(Block):
"run_continuously": False, "run_continuously": False,
}, },
test_output=[ test_output=[
(
"entries",
[
RSSEntry(
title="Example RSS Item",
link="https://example.com/article",
description="This is an example RSS item description.",
pub_date=datetime(
2023, 6, 23, 12, 30, 0, tzinfo=timezone.utc
),
author="John Doe",
categories=["Technology", "News"],
),
],
),
( (
"entry", "entry",
RSSEntry( RSSEntry(
@@ -96,21 +112,22 @@ class ReadRSSFeedBlock(Block):
keep_going = input_data.run_continuously keep_going = input_data.run_continuously
feed = self.parse_feed(input_data.rss_url) feed = self.parse_feed(input_data.rss_url)
all_entries = []
for entry in feed["entries"]: for entry in feed["entries"]:
pub_date = datetime(*entry["published_parsed"][:6], tzinfo=timezone.utc) pub_date = datetime(*entry["published_parsed"][:6], tzinfo=timezone.utc)
if pub_date > start_time: if pub_date > start_time:
yield ( rss_entry = RSSEntry(
"entry", title=entry["title"],
RSSEntry( link=entry["link"],
title=entry["title"], description=entry.get("summary", ""),
link=entry["link"], pub_date=pub_date,
description=entry.get("summary", ""), author=entry.get("author", ""),
pub_date=pub_date, categories=[tag["term"] for tag in entry.get("tags", [])],
author=entry.get("author", ""),
categories=[tag["term"] for tag in entry.get("tags", [])],
),
) )
all_entries.append(rss_entry)
yield "entry", rss_entry
yield "entries", all_entries
await asyncio.sleep(input_data.polling_rate) await asyncio.sleep(input_data.polling_rate)