Skip to content

Commit 8a0eaad

Browse files
feat: add minimal feedback collection tool
- Add feedback tool that sends data to API endpoint - No LangSmith integration - just API calls - Simple validation with Pydantic - Auto-included in StackOneToolSet - 6 focused tests covering validation, execution, and integration
1 parent 983e2f7 commit 8a0eaad

File tree

4 files changed

+391
-0
lines changed

4 files changed

+391
-0
lines changed

stackone_ai/feedback/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Feedback collection tools for StackOne."""
2+
3+
from .tool import create_feedback_tool
4+
5+
__all__ = ["create_feedback_tool"]

stackone_ai/feedback/tool.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Feedback collection tool for StackOne."""
2+
3+
# TODO: Remove when Python 3.9 support is dropped
4+
from __future__ import annotations
5+
6+
import json
7+
8+
from pydantic import BaseModel, Field, field_validator
9+
10+
from ..models import (
11+
ExecuteConfig,
12+
JsonDict,
13+
ParameterLocation,
14+
StackOneError,
15+
StackOneTool,
16+
ToolParameters,
17+
)
18+
19+
20+
class FeedbackInput(BaseModel):
21+
"""Input schema for feedback tool."""
22+
23+
feedback: str = Field(..., min_length=1, description="User feedback text")
24+
account_id: str = Field(..., min_length=1, description="Account identifier")
25+
tool_names: list[str] = Field(..., min_length=1, description="List of tool names")
26+
27+
@field_validator("feedback", "account_id")
28+
@classmethod
29+
def validate_non_empty_trimmed(cls, v: str) -> str:
30+
"""Validate that string is non-empty after trimming."""
31+
trimmed = v.strip()
32+
if not trimmed:
33+
raise ValueError("Field must be a non-empty string")
34+
return trimmed
35+
36+
@field_validator("tool_names")
37+
@classmethod
38+
def validate_tool_names(cls, v: list[str]) -> list[str]:
39+
"""Validate and clean tool names."""
40+
cleaned = [name.strip() for name in v if name.strip()]
41+
if not cleaned:
42+
raise ValueError("At least one tool name is required")
43+
return cleaned
44+
45+
46+
class FeedbackTool(StackOneTool):
47+
"""Extended tool for collecting feedback with enhanced validation."""
48+
49+
def execute(self, arguments: str | JsonDict | None = None) -> JsonDict:
50+
"""
51+
Execute the feedback tool with enhanced validation.
52+
53+
Args:
54+
arguments: Tool arguments as string or dict
55+
56+
Returns:
57+
Response from the API
58+
59+
Raises:
60+
StackOneError: If validation or API call fails
61+
"""
62+
try:
63+
# Parse input
64+
if isinstance(arguments, str):
65+
raw_params = json.loads(arguments)
66+
else:
67+
raw_params = arguments or {}
68+
69+
# Validate with Pydantic
70+
parsed_params = FeedbackInput(**raw_params)
71+
72+
# Build validated request body
73+
validated_arguments = {
74+
"feedback": parsed_params.feedback,
75+
"account_id": parsed_params.account_id,
76+
"tool_names": parsed_params.tool_names,
77+
}
78+
79+
# Use the parent execute method with validated arguments
80+
return super().execute(validated_arguments)
81+
82+
except json.JSONDecodeError as exc:
83+
raise StackOneError(f"Invalid JSON in arguments: {exc}") from exc
84+
except ValueError as exc:
85+
raise StackOneError(f"Validation error: {exc}") from exc
86+
except Exception as error:
87+
if isinstance(error, StackOneError):
88+
raise
89+
raise StackOneError(f"Error executing feedback tool: {error}") from error
90+
91+
92+
def create_feedback_tool(
93+
api_key: str,
94+
account_id: str | None = None,
95+
base_url: str = "https://api.stackone.com",
96+
) -> FeedbackTool:
97+
"""
98+
Create a feedback collection tool.
99+
100+
Args:
101+
api_key: API key for authentication
102+
account_id: Optional account ID
103+
base_url: Base URL for the API
104+
105+
Returns:
106+
FeedbackTool configured for feedback collection
107+
"""
108+
name = "meta_collect_tool_feedback"
109+
description = (
110+
"Collects user feedback on StackOne tool performance. "
111+
"First ask the user, \"Are you ok with sending feedback to StackOne?\" "
112+
"and mention that the LLM will take care of sending it. "
113+
"Call this tool only when the user explicitly answers yes."
114+
)
115+
116+
parameters = ToolParameters(
117+
type="object",
118+
properties={
119+
"account_id": {
120+
"type": "string",
121+
"description": 'Account identifier (e.g., "acc_123456")',
122+
},
123+
"feedback": {
124+
"type": "string",
125+
"description": "Verbatim feedback from the user about their experience with StackOne tools.",
126+
},
127+
"tool_names": {
128+
"type": "array",
129+
"items": {
130+
"type": "string",
131+
},
132+
"description": "Array of tool names being reviewed",
133+
},
134+
},
135+
)
136+
137+
execute_config = ExecuteConfig(
138+
name=name,
139+
method="POST",
140+
url=f"{base_url}/ai/tool-feedback",
141+
body_type="json",
142+
parameter_locations={
143+
"feedback": ParameterLocation.BODY,
144+
"account_id": ParameterLocation.BODY,
145+
"tool_names": ParameterLocation.BODY,
146+
},
147+
)
148+
149+
# Create instance by calling parent class __init__ directly since FeedbackTool is a subclass
150+
tool = FeedbackTool.__new__(FeedbackTool)
151+
StackOneTool.__init__(
152+
tool,
153+
description=description,
154+
parameters=parameters,
155+
_execute_config=execute_config,
156+
_api_key=api_key,
157+
_account_id=account_id,
158+
)
159+
160+
return tool

stackone_ai/toolset.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,17 @@ def get_tools(
170170
)
171171
all_tools.append(tool)
172172

173+
# Add feedback collection meta tool
174+
from .feedback import create_feedback_tool
175+
feedback_tool_name = "meta_collect_tool_feedback"
176+
if filter_pattern is None or self._matches_filter(feedback_tool_name, filter_pattern):
177+
feedback_tool = create_feedback_tool(
178+
api_key=self.api_key,
179+
account_id=effective_account_id,
180+
base_url=self.base_url or "https://api.stackone.com",
181+
)
182+
all_tools.append(feedback_tool)
183+
173184
return Tools(all_tools)
174185

175186
except Exception as e:

0 commit comments

Comments
 (0)