Skip to content

Commit 4f91da8

Browse files
committed
feat(tools): Make the applied filters explicit as a return to the model
Issue: APPAI-46
1 parent 8ce8356 commit 4f91da8

File tree

3 files changed

+164
-15
lines changed

3 files changed

+164
-15
lines changed

packages/gg_api_core/src/gg_api_core/tools/list_repo_incidents.py

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,64 @@
3333
] # We exclude "INVALID" ones
3434

3535

36+
def _build_filter_info(params: "ListRepoIncidentsParams") -> dict[str, Any]:
37+
"""Build a dictionary describing the filters applied to the query."""
38+
filters = {}
39+
40+
# Include all active filters
41+
if params.from_date:
42+
filters["from_date"] = params.from_date
43+
if params.to_date:
44+
filters["to_date"] = params.to_date
45+
if params.presence:
46+
filters["presence"] = params.presence
47+
if params.tags:
48+
filters["tags_include"] = [tag.value if hasattr(tag, 'value') else tag for tag in params.tags]
49+
if params.exclude_tags:
50+
filters["exclude_tags"] = [tag.value if hasattr(tag, 'value') else tag for tag in params.exclude_tags]
51+
if params.status:
52+
filters["status"] = [st.value if hasattr(st, 'value') else st for st in params.status]
53+
if params.severity:
54+
filters["severity"] = [sev.value if hasattr(sev, 'value') else sev for sev in params.severity]
55+
if params.validity:
56+
filters["validity"] = [v.value if hasattr(v, 'value') else v for v in params.validity]
57+
if not params.mine:
58+
filters["assigned_to_me"] = False
59+
60+
return filters
61+
62+
63+
def _build_suggestion(params: "ListRepoIncidentsParams", incidents_count: int) -> str:
64+
"""Build a suggestion message based on applied filters and results."""
65+
suggestions = []
66+
67+
# Explain what's being filtered
68+
if params.mine:
69+
suggestions.append("Filtering to incidents assigned to current user")
70+
71+
if params.exclude_tags:
72+
excluded_tag_names = [tag.name if hasattr(tag, 'name') else tag for tag in params.exclude_tags]
73+
suggestions.append(f"Incidents are filtered to exclude tags: {', '.join(excluded_tag_names)}")
74+
75+
if params.status:
76+
status_names = [st.name if hasattr(st, 'name') else st for st in params.status]
77+
suggestions.append(f"Filtered by status: {', '.join(status_names)}")
78+
79+
if params.severity:
80+
sev_names = [sev.name if hasattr(sev, 'name') else sev for sev in params.severity]
81+
suggestions.append(f"Filtered by severity: {', '.join(sev_names)}")
82+
83+
if params.validity:
84+
val_names = [v.name if hasattr(v, 'name') else v for v in params.validity]
85+
suggestions.append(f"Filtered by validity: {', '.join(val_names)}")
86+
87+
# If no results, suggest how to get more
88+
if incidents_count == 0 and suggestions:
89+
suggestions.append("No incidents matched the applied filters. Try with mine=False, exclude_tags=[], or different status/severity/validity filters to see all incidents.")
90+
91+
return "\n".join(suggestions) if suggestions else ""
92+
93+
3694
class ListRepoIncidentsParams(BaseModel):
3795
"""Parameters for listing repository incidents."""
3896
repository_name: str | None = Field(
@@ -43,6 +101,12 @@ class ListRepoIncidentsParams(BaseModel):
43101
default=None,
44102
description="The GitGuardian source ID to filter by. Can be obtained using find_current_source_id. If provided, repository_name is not required."
45103
)
104+
ordering: str | None = Field(default=None, description="Sort field (e.g., 'date', '-date' for descending)")
105+
per_page: int = Field(default=20, description="Number of results per page (default: 20, min: 1, max: 100)")
106+
cursor: str | None = Field(default=None, description="Pagination cursor for fetching next page of results")
107+
get_all: bool = Field(default=False, description="If True, fetch all results using cursor-based pagination")
108+
109+
# Filters
46110
from_date: str | None = Field(
47111
default=None, description="Filter occurrences created after this date (ISO format: YYYY-MM-DD)"
48112
)
@@ -56,10 +120,6 @@ class ListRepoIncidentsParams(BaseModel):
56120
description="Exclude incidents with these tag names."
57121
)
58122
status: list[str] | None = Field(default=DEFAULT_STATUSES, description="Filter by status (list of status names)")
59-
ordering: str | None = Field(default=None, description="Sort field (e.g., 'date', '-date' for descending)")
60-
per_page: int = Field(default=20, description="Number of results per page (default: 20, min: 1, max: 100)")
61-
cursor: str | None = Field(default=None, description="Pagination cursor for fetching next page of results")
62-
get_all: bool = Field(default=False, description="If True, fetch all results using cursor-based pagination")
63123
mine: bool = Field(
64124
default=True,
65125
description="If True, fetch only incidents assigned to the current user. Set to False to get all incidents.",
@@ -124,16 +184,22 @@ async def list_repo_incidents(params: ListRepoIncidentsParams) -> dict[str, Any]
124184
if params.get_all:
125185
incidents_result = await client.paginate_all(f"/sources/{params.source_id}/incidents/secrets", api_params)
126186
if isinstance(incidents_result, list):
187+
count = len(incidents_result)
127188
return {
128189
"source_id": params.source_id,
129190
"incidents": incidents_result,
130-
"total_count": len(incidents_result),
191+
"total_count": count,
192+
"applied_filters": _build_filter_info(params),
193+
"suggestion": _build_suggestion(params, count),
131194
}
132195
elif isinstance(incidents_result, dict):
196+
count = incidents_result.get("total_count", len(incidents_result.get("data", [])))
133197
return {
134198
"source_id": params.source_id,
135199
"incidents": incidents_result.get("data", []),
136-
"total_count": incidents_result.get("total_count", len(incidents_result.get("data", []))),
200+
"total_count": count,
201+
"applied_filters": _build_filter_info(params),
202+
"suggestion": _build_suggestion(params, count),
137203
}
138204
else:
139205
# Fallback for unexpected types
@@ -142,22 +208,30 @@ async def list_repo_incidents(params: ListRepoIncidentsParams) -> dict[str, Any]
142208
"incidents": [],
143209
"total_count": 0,
144210
"error": f"Unexpected response type: {type(incidents_result).__name__}",
211+
"applied_filters": _build_filter_info(params),
212+
"suggestion": _build_suggestion(params, 0),
145213
}
146214
else:
147215
incidents_result = await client.list_source_incidents(params.source_id, **api_params)
148216
if isinstance(incidents_result, dict):
217+
count = incidents_result.get("total_count", 0)
149218
return {
150219
"source_id": params.source_id,
151220
"incidents": incidents_result.get("data", []),
152221
"next_cursor": incidents_result.get("next_cursor"),
153-
"total_count": incidents_result.get("total_count", 0),
222+
"total_count": count,
223+
"applied_filters": _build_filter_info(params),
224+
"suggestion": _build_suggestion(params, count),
154225
}
155226
elif isinstance(incidents_result, list):
156227
# Handle case where API returns a list directly
228+
count = len(incidents_result)
157229
return {
158230
"source_id": params.source_id,
159231
"incidents": incidents_result,
160-
"total_count": len(incidents_result),
232+
"total_count": count,
233+
"applied_filters": _build_filter_info(params),
234+
"suggestion": _build_suggestion(params, count),
161235
}
162236
else:
163237
# Fallback for unexpected types
@@ -166,6 +240,8 @@ async def list_repo_incidents(params: ListRepoIncidentsParams) -> dict[str, Any]
166240
"incidents": [],
167241
"total_count": 0,
168242
"error": f"Unexpected response type: {type(incidents_result).__name__}",
243+
"applied_filters": _build_filter_info(params),
244+
"suggestion": _build_suggestion(params, 0),
169245
}
170246
else:
171247
# Use repository_name lookup (legacy path)
@@ -182,6 +258,13 @@ async def list_repo_incidents(params: ListRepoIncidentsParams) -> dict[str, Any]
182258
get_all=params.get_all,
183259
mine=params.mine,
184260
)
261+
262+
# Enrich result with filter info
263+
if isinstance(result, dict):
264+
count = result.get("total_count", len(result.get("incidents", [])))
265+
result["applied_filters"] = _build_filter_info(params)
266+
result["suggestion"] = _build_suggestion(params, count)
267+
185268
return result
186269

187270
except Exception as e:

packages/gg_api_core/src/gg_api_core/tools/list_repo_occurrences.py

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ class ListRepoOccurrencesParams(BaseModel):
4545
default=None,
4646
description="The GitGuardian source ID to filter by. Can be obtained using find_current_source_id. If provided, repository_name is not required."
4747
)
48+
ordering: str | None = Field(default=None, description="Sort field (e.g., 'date', '-date' for descending)")
49+
per_page: int = Field(default=20, description="Number of results per page (default: 20, min: 1, max: 100)")
50+
cursor: str | None = Field(default=None, description="Pagination cursor for fetching next page of results")
51+
get_all: bool = Field(default=False, description="If True, fetch all results using cursor-based pagination")
52+
53+
# Filters
4854
from_date: str | None = Field(
4955
default=None, description="Filter occurrences created after this date (ISO format: YYYY-MM-DD)"
5056
)
@@ -57,15 +63,64 @@ class ListRepoOccurrencesParams(BaseModel):
5763
default=DEFAULT_EXCLUDED_TAGS,
5864
description="Exclude occurrences with these tag names. Pass empty list to disable filtering."
5965
)
60-
ordering: str | None = Field(default=None, description="Sort field (e.g., 'date', '-date' for descending)")
61-
per_page: int = Field(default=20, description="Number of results per page (default: 20, min: 1, max: 100)")
62-
cursor: str | None = Field(default=None, description="Pagination cursor for fetching next page of results")
63-
get_all: bool = Field(default=False, description="If True, fetch all results using cursor-based pagination")
6466
status: list[str] | None = Field(default=DEFAULT_STATUSES, description="Filter by status (list of status names)")
6567
severity: list[str] | None = Field(default=DEFAULT_SEVERITIES, description="Filter by severity (list of severity names)")
6668
validity: list[str] | None = Field(default=DEFAULT_VALIDITIES, description="Filter by validity (list of validity names)")
6769

6870

71+
def _build_filter_info(params: ListRepoOccurrencesParams) -> dict[str, Any]:
72+
"""Build a dictionary describing the filters applied to the query."""
73+
filters = {}
74+
75+
# Include all active filters
76+
if params.from_date:
77+
filters["from_date"] = params.from_date
78+
if params.to_date:
79+
filters["to_date"] = params.to_date
80+
if params.presence:
81+
filters["presence"] = params.presence
82+
if params.tags:
83+
filters["tags"] = [tag.value if hasattr(tag, 'value') else tag for tag in params.tags]
84+
if params.exclude_tags:
85+
filters["exclude_tags"] = [tag.value if hasattr(tag, 'value') else tag for tag in params.exclude_tags]
86+
if params.status:
87+
filters["status"] = [st.value if hasattr(st, 'value') else st for st in params.status]
88+
if params.severity:
89+
filters["severity"] = [sev.value if hasattr(sev, 'value') else sev for sev in params.severity]
90+
if params.validity:
91+
filters["validity"] = [v.value if hasattr(v, 'value') else v for v in params.validity]
92+
93+
return filters
94+
95+
96+
def _build_suggestion(params: ListRepoOccurrencesParams, occurrences_count: int) -> str:
97+
"""Build a suggestion message based on applied filters and results."""
98+
suggestions = []
99+
100+
# Explain what's being filtered
101+
if params.exclude_tags:
102+
excluded_tag_names = [tag.name if hasattr(tag, 'name') else tag for tag in params.exclude_tags]
103+
suggestions.append(f"Occurrences were filtered to exclude tags: {', '.join(excluded_tag_names)}")
104+
105+
if params.status:
106+
status_names = [st.name if hasattr(st, 'name') else st for st in params.status]
107+
suggestions.append(f"Filtered by status: {', '.join(status_names)}")
108+
109+
if params.severity:
110+
sev_names = [sev.name if hasattr(sev, 'name') else sev for sev in params.severity]
111+
suggestions.append(f"Filtered by severity: {', '.join(sev_names)}")
112+
113+
if params.validity:
114+
val_names = [v.name if hasattr(v, 'name') else v for v in params.validity]
115+
suggestions.append(f"Filtered by validity: {', '.join(val_names)}")
116+
117+
# If no results, suggest how to get more
118+
if occurrences_count == 0 and suggestions:
119+
suggestions.append("No occurrences matched the applied filters. Try with exclude_tags=[] or different status/severity/validity filters to see all occurrences.")
120+
121+
return "\n".join(suggestions) if suggestions else ""
122+
123+
69124
async def list_repo_occurrences(params: ListRepoOccurrencesParams) -> dict[str, Any]:
70125
"""
71126
List secret occurrences for a specific repository using the GitGuardian v1/occurrences/secrets API.
@@ -91,7 +146,8 @@ async def list_repo_occurrences(params: ListRepoOccurrencesParams) -> dict[str,
91146
params: ListRepoOccurrencesParams model containing all filtering options
92147
93148
Returns:
94-
List of secret occurrences with detailed match information including file locations and indices
149+
List of secret occurrences with detailed match information including file locations and indices.
150+
Also includes applied filters and suggestions for interpreting the results.
95151
"""
96152
client = get_client()
97153

@@ -143,25 +199,33 @@ async def list_repo_occurrences(params: ListRepoOccurrencesParams) -> dict[str,
143199
# Handle the response format
144200
if isinstance(result, dict):
145201
occurrences = result.get("occurrences", [])
202+
count = len(occurrences)
146203
return {
147204
"repository": params.repository_name,
148-
"occurrences_count": len(occurrences),
205+
"occurrences_count": count,
149206
"occurrences": occurrences,
150207
"cursor": result.get("cursor"),
151208
"has_more": result.get("has_more", False),
209+
"applied_filters": _build_filter_info(params),
210+
"suggestion": _build_suggestion(params, count),
152211
}
153212
elif isinstance(result, list):
154213
# If get_all=True, we get a list directly
214+
count = len(result)
155215
return {
156216
"repository": params.repository_name,
157-
"occurrences_count": len(result),
217+
"occurrences_count": count,
158218
"occurrences": result,
219+
"applied_filters": _build_filter_info(params),
220+
"suggestion": _build_suggestion(params, count),
159221
}
160222
else:
161223
return {
162224
"repository": params.repository_name,
163225
"occurrences_count": 0,
164226
"occurrences": [],
227+
"applied_filters": _build_filter_info(params),
228+
"suggestion": _build_suggestion(params, 0),
165229
}
166230

167231
except Exception as e:

packages/gg_api_core/src/gg_api_core/tools/remediate_secret_incidents.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ async def remediate_secret_incidents(params: RemediateSecretIncidentsParams) ->
122122
"repository_info": {"name": params.repository_name},
123123
"message": "No secret occurrences found for this repository that match the criteria.",
124124
"remediation_steps": [],
125+
"applied_filters": occurrences_result.get("applied_filters", {}),
126+
"suggestion": occurrences_result.get("suggestion", ""),
125127
}
126128

127129
# Process occurrences for remediation with exact location data

0 commit comments

Comments
 (0)