Skip to content

Commit 99ba933

Browse files
authored
Merge pull request #11 from geekbot-com/feature/polls
Feature/polls
2 parents f2aab7c + bffc49e commit 99ba933

File tree

12 files changed

+477
-29
lines changed

12 files changed

+477
-29
lines changed

.github/workflows/test.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Python Tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v3
12+
13+
- name: Set up Python
14+
uses: actions/setup-python@v4
15+
with:
16+
python-version: '3.10' # You might want to adjust this or use a matrix strategy
17+
18+
- name: Verify Python version compatibility
19+
run: |
20+
PYTHON_REQUIREMENT=$(grep "requires-python" pyproject.toml | sed -E 's/.*requires-python = "(.*)".*$/\1/')
21+
CURRENT_PYTHON_VERSION=$(python --version | cut -d ' ' -f 2)
22+
echo "Checking if Python $CURRENT_PYTHON_VERSION satisfies requirement: $PYTHON_REQUIREMENT"
23+
24+
pip install packaging
25+
26+
python -c "from packaging.specifiers import SpecifierSet; import sys; requirement='$PYTHON_REQUIREMENT'; version='$CURRENT_PYTHON_VERSION'; exit(0 if SpecifierSet(requirement).contains(version) else 1)"
27+
28+
if [ $? -eq 0 ]; then
29+
echo "✅ Python version $CURRENT_PYTHON_VERSION is compatible with requirement $PYTHON_REQUIREMENT"
30+
else
31+
echo "❌ Python version $CURRENT_PYTHON_VERSION is NOT compatible with requirement $PYTHON_REQUIREMENT"
32+
exit 1
33+
fi
34+
35+
- name: Install uv
36+
run: |
37+
pip install uv
38+
39+
- name: Install dependencies
40+
run: |
41+
uv pip install --system .[dev]
42+
uv pip install -r pyproject.toml --group dev --system
43+
44+
- name: Run Pytest
45+
run: |
46+
python -m pytest -vv

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,22 @@ Once configured, your LLM client application will have access to the following t
104104
- `participants`: List of users participating in the standup.
105105
- `owner_id`: ID of the standup owner.
106106
107+
- `list_polls`
108+
109+
**Purpose:** Lists all the polls accessible via your API key. Useful for getting an overview or finding a specific poll ID.
110+
111+
**Example Prompt:** "Hey, can you list my Geekbot polls?"
112+
113+
**Data Fields Returned:**
114+
115+
- `id`: Unique poll identifier.
116+
- `name`: Name of the poll.
117+
- `time`: Scheduled time for the poll.
118+
- `timezone`: Timezone for the scheduled time.
119+
- `questions`: List of questions asked in the poll.
120+
- `participants`: List of users participating in the poll.
121+
- `creator`: The poll creator.
122+
107123
- `fetch_reports`
108124
109125
**Purpose:** Retrieves specific standup reports. You can filter by standup, user, and date range.
@@ -143,6 +159,17 @@ Once configured, your LLM client application will have access to the following t
143159
- `email`: Member's email address.
144160
- `role`: Member's role within Geekbot (e.g., Admin, Member).
145161
162+
- `fetch_poll_results`
163+
164+
**Purpose:** Retrieves specific poll results. Requires a poll id and optionally a date range.
165+
166+
**Example Prompt:** "Hey, what was decided about the new logo in Geekbot polls?"
167+
168+
**Data Fields Returned:**
169+
170+
- `total_results`: Total number of results.
171+
- `question_results`: List of question results.
172+
146173
### Prompts 💬
147174
148175
- `weekly_rollup_report`

geekbot_mcp/gb_api.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ async def get_standups(
2020
response.raise_for_status()
2121
return response.json()
2222

23+
async def get_polls(self) -> list:
24+
"""Get list of polls"""
25+
endpoint = f"{self.base_url}/polls/"
26+
response = await self._client.get(endpoint, headers=self.headers)
27+
response.raise_for_status()
28+
return response.json()
29+
2330
async def get_reports(
2431
self,
2532
standup_id: int | None = None,
@@ -48,5 +55,20 @@ async def get_reports(
4855
response.raise_for_status()
4956
return response.json()
5057

58+
async def get_poll_results(
59+
self, poll_id: int, after: int | None = None, before: int | None = None
60+
) -> dict:
61+
"""Fetch poll results"""
62+
endpoint = f"{self.base_url}/polls/{poll_id}/votes/"
63+
params = {}
64+
if after:
65+
params["after"] = after
66+
if before:
67+
params["before"] = before
68+
69+
response = await self._client.get(endpoint, headers=self.headers, params=params)
70+
response.raise_for_status()
71+
return response.json()
72+
5173
def close(self):
5274
self._client.close()

geekbot_mcp/models.py

Lines changed: 108 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
11
from datetime import datetime
22

3-
from pydantic import BaseModel
3+
from pydantic import BaseModel, ConfigDict
44

55

6-
class Participant(BaseModel):
6+
class User(BaseModel):
7+
model_config = ConfigDict(frozen=True)
8+
79
id: str
810
name: str
911
username: str
1012
email: str
1113
role: str
1214

13-
class Config:
14-
frozen = True
15-
1615
def __hash__(self):
1716
return hash(self.id)
1817

1918

2019
class Reporter(BaseModel):
20+
model_config = ConfigDict(frozen=True)
21+
2122
id: str
2223
name: str
2324
username: str
2425

25-
class Config:
26-
frozen = True
27-
2826
def __hash__(self):
2927
return hash(self.id)
3028

@@ -37,43 +35,78 @@ class Question(BaseModel):
3735

3836

3937
class Standup(BaseModel):
38+
model_config = ConfigDict(frozen=True)
39+
4040
id: int
4141
name: str
4242
channel: str
4343
time: str
4444
timezone: str
4545
questions: list[Question]
46-
participants: list[Participant]
46+
participants: list[User]
4747
owner_id: str
4848

49-
class Config:
50-
frozen = True
49+
def __hash__(self):
50+
return hash(self.id)
51+
52+
53+
class Poll(BaseModel):
54+
model_config = ConfigDict(frozen=True)
55+
56+
id: int
57+
name: str
58+
time: str
59+
timezone: str
60+
questions: list[Question]
61+
participants: list[User]
62+
creator: User
5163

5264
def __hash__(self):
5365
return hash(self.id)
5466

5567

5668
class Report(BaseModel):
69+
model_config = ConfigDict(frozen=True)
70+
5771
id: int
5872
standup_id: int
5973
created_at: str
6074
reporter: Reporter
6175
content: str
6276

63-
class Config:
64-
frozen = True
65-
6677
def __hash__(self):
6778
return hash(self.id)
6879

6980

70-
def participant_from_json_response(p_res: dict) -> Participant:
71-
return Participant(
72-
id=p_res["id"],
73-
name=p_res["realname"],
74-
username=p_res["username"],
75-
email=p_res["email"],
76-
role=p_res["role"],
81+
class PollChoiceResult(BaseModel):
82+
text: str
83+
votes: int
84+
percentage: float
85+
users: list[User]
86+
87+
88+
class PollQuestionResult(BaseModel):
89+
date: str
90+
choices: list[PollChoiceResult]
91+
92+
93+
class PollQuestionResults(BaseModel):
94+
question_text: str
95+
results: list[PollQuestionResult]
96+
97+
98+
class PollResults(BaseModel):
99+
num_poll_instances: int
100+
question_results: list[PollQuestionResults]
101+
102+
103+
def user_from_json_response(u_res: dict) -> User:
104+
return User(
105+
id=u_res["id"],
106+
name=u_res["realname"],
107+
username=u_res["username"],
108+
email=u_res["email"],
109+
role=u_res["role"],
77110
)
78111

79112

@@ -90,6 +123,15 @@ def question_from_json_response(q_res: dict) -> Question:
90123
)
91124

92125

126+
def poll_question_from_json_response(q_res: dict) -> Question:
127+
return Question(
128+
text=q_res["text"],
129+
answer_type=q_res["answer_type"],
130+
answer_choices=q_res["answer_choices"],
131+
is_random=False,
132+
)
133+
134+
93135
def standup_from_json_response(s_res: dict) -> Standup:
94136
return Standup(
95137
id=s_res["id"],
@@ -98,11 +140,23 @@ def standup_from_json_response(s_res: dict) -> Standup:
98140
time=s_res["time"],
99141
timezone=s_res["timezone"],
100142
questions=[question_from_json_response(q) for q in s_res["questions"]],
101-
participants=[participant_from_json_response(p) for p in s_res["users"]],
143+
participants=[user_from_json_response(p) for p in s_res["users"]],
102144
owner_id=s_res["master"],
103145
)
104146

105147

148+
def poll_from_json_response(p_res: dict) -> Poll:
149+
return Poll(
150+
id=p_res["id"],
151+
name=p_res["name"],
152+
time=p_res["time"],
153+
timezone=p_res["timezone"],
154+
questions=[poll_question_from_json_response(q) for q in p_res["questions"]],
155+
participants=[user_from_json_response(p) for p in p_res["users"]],
156+
creator=user_from_json_response(p_res["creator"]),
157+
)
158+
159+
106160
def reporter_from_json_response(r_res: dict) -> Reporter:
107161
return Reporter(
108162
id=r_res["id"],
@@ -129,3 +183,35 @@ def report_from_json_response(r_res: dict) -> Report:
129183
reporter=reporter_from_json_response(r_res["member"]),
130184
content=content_from_json_response(r_res["questions"]),
131185
)
186+
187+
188+
def poll_choice_result_from_json_response(c_res: dict) -> PollChoiceResult:
189+
return PollChoiceResult(
190+
text=c_res["text"],
191+
votes=c_res["votes"],
192+
percentage=c_res["percentage"],
193+
users=[user_from_json_response(u) for u in c_res["users"]],
194+
)
195+
196+
197+
def poll_question_result_from_json_response(q_res: dict) -> PollQuestionResult:
198+
return PollQuestionResult(
199+
date=q_res["date"],
200+
choices=[poll_choice_result_from_json_response(c) for c in q_res["answers"]],
201+
)
202+
203+
204+
def poll_question_results_from_json_response(q_res: dict) -> PollQuestionResults:
205+
return PollQuestionResults(
206+
question_text=q_res["text"],
207+
results=[poll_question_result_from_json_response(r) for r in q_res["results"]],
208+
)
209+
210+
211+
def poll_results_from_json_response(p_res: dict) -> PollResults:
212+
return PollResults(
213+
num_poll_instances=p_res["total_results"],
214+
question_results=[
215+
poll_question_results_from_json_response(q) for q in p_res["questions"]
216+
],
217+
)

geekbot_mcp/tools/__init__.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
from mcp import types
22

33
from geekbot_mcp.gb_api import GeekbotClient
4+
from geekbot_mcp.tools.fetch_poll_results import (
5+
fetch_poll_results,
6+
handle_fetch_poll_results,
7+
)
48
from geekbot_mcp.tools.fetch_reports import fetch_reports, handle_fetch_reports
59
from geekbot_mcp.tools.list_members import handle_list_members, list_members
10+
from geekbot_mcp.tools.list_polls import handle_list_polls, list_polls
611
from geekbot_mcp.tools.list_standups import handle_list_standups, list_standups
712

813

914
def list_tools() -> list[types.Tool]:
10-
return [fetch_reports, list_standups, list_members]
15+
return [
16+
list_members,
17+
list_standups,
18+
fetch_reports,
19+
list_polls,
20+
fetch_poll_results,
21+
]
1122

1223

1324
async def run_tool(
@@ -16,11 +27,15 @@ async def run_tool(
1627
arguments: dict[str, str] | None,
1728
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
1829
match name:
19-
case "fetch_reports":
20-
return await handle_fetch_reports(gb_client, **arguments)
21-
case "list_standups":
22-
return await handle_list_standups(gb_client)
2330
case "list_members":
2431
return await handle_list_members(gb_client)
32+
case "list_standups":
33+
return await handle_list_standups(gb_client)
34+
case "fetch_reports":
35+
return await handle_fetch_reports(gb_client, **arguments)
36+
case "list_polls":
37+
return await handle_list_polls(gb_client)
38+
case "fetch_poll_results":
39+
return await handle_fetch_poll_results(gb_client, **arguments)
2540
case _:
2641
raise ValueError(f"Tool {name} not found")

0 commit comments

Comments
 (0)