hf-agentfinder 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. hf_agentfinder-0.1.0/.fast-agent/.check_for_update_done +0 -0
  2. hf_agentfinder-0.1.0/.fast-agent/agent-cards/dev.md +42 -0
  3. hf_agentfinder-0.1.0/.fast-agent/agent-cards/multilspy_tools.py +421 -0
  4. hf_agentfinder-0.1.0/.fast-agent/fast-agent.yaml +3 -0
  5. hf_agentfinder-0.1.0/.fast-agent/sessions/2605121105-LFZaCv/history_agent.json +1348 -0
  6. hf_agentfinder-0.1.0/.fast-agent/sessions/2605121105-LFZaCv/history_agent_previous.json +1256 -0
  7. hf_agentfinder-0.1.0/.fast-agent/sessions/2605121105-LFZaCv/session.json +61 -0
  8. hf_agentfinder-0.1.0/.fast-agent/sessions/2605121135-j3aamD/history_dev.json +4135 -0
  9. hf_agentfinder-0.1.0/.fast-agent/sessions/2605121135-j3aamD/history_dev_previous.json +4053 -0
  10. hf_agentfinder-0.1.0/.fast-agent/sessions/2605121135-j3aamD/session.json +65 -0
  11. hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/.skill-source.json +14 -0
  12. hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/SKILL.md +195 -0
  13. hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/python/dev.md +32 -0
  14. hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/python/multilspy_tools.py +421 -0
  15. hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/rust/dev.md +32 -0
  16. hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/rust/rust_lsp_tools.py +589 -0
  17. hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/typescript/dev.md +32 -0
  18. hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/assets/typescript/multilspy_tools.py +424 -0
  19. hf_agentfinder-0.1.0/.fast-agent/skills/lsp-setup/references/rust.md +131 -0
  20. hf_agentfinder-0.1.0/.github/workflows/ci.yml +45 -0
  21. hf_agentfinder-0.1.0/.github/workflows/release.yml +50 -0
  22. hf_agentfinder-0.1.0/.gitignore +8 -0
  23. hf_agentfinder-0.1.0/AGENTS.md +3 -0
  24. hf_agentfinder-0.1.0/PKG-INFO +91 -0
  25. hf_agentfinder-0.1.0/README.md +78 -0
  26. hf_agentfinder-0.1.0/pyproject.toml +91 -0
  27. hf_agentfinder-0.1.0/scripts/release-local.sh +98 -0
  28. hf_agentfinder-0.1.0/spec/agentfinder.md +567 -0
  29. hf_agentfinder-0.1.0/src/agentfinder/__init__.py +1 -0
  30. hf_agentfinder-0.1.0/src/agentfinder/cli.py +152 -0
  31. hf_agentfinder-0.1.0/src/agentfinder/hf_spaces.py +269 -0
  32. hf_agentfinder-0.1.0/src/agentfinder/models.py +56 -0
  33. hf_agentfinder-0.1.0/src/agentfinder/server.py +121 -0
  34. hf_agentfinder-0.1.0/tests/test_hf_spaces.py +173 -0
  35. hf_agentfinder-0.1.0/typesafe.md +25 -0
  36. hf_agentfinder-0.1.0/uv.lock +523 -0
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: dev
3
+ # Leave this at the configured system default unless you want to pin a model.
4
+ model: $system.default
5
+ default: true
6
+ shell: true
7
+ function_tools:
8
+ - multilspy_tools.py:lsp_hover
9
+ - multilspy_tools.py:lsp_definition
10
+ - multilspy_tools.py:lsp_references
11
+ - multilspy_tools.py:lsp_document_symbols
12
+ - multilspy_tools.py:lsp_workspace_symbols
13
+ - multilspy_tools.py:lsp_diagnostics
14
+ ---
15
+
16
+ You are a development assistant for this Python project.
17
+
18
+
19
+ ## Code Navigation
20
+
21
+ Use LSP tools for structural queries: definitions, references, symbols, hover info, diagnostics.
22
+ For broad text discovery or file operations, use whatever search tool or card is already available in this environment.
23
+
24
+ {{serverInstructions}}
25
+ {{agentSkills}}
26
+ {{env}}
27
+
28
+ Note any project specific instructions or details here:
29
+
30
+ ---
31
+
32
+ {{file_silent:AGENTS.md}}
33
+
34
+ {{file_silent:pyproject.toml}}
35
+
36
+ ---
37
+
38
+ Mermaid diagrams between code fences are supported.
39
+
40
+ {{model_specific}}
41
+
42
+ The current date is {{currentDate}}.
@@ -0,0 +1,421 @@
1
+ """Function tools for ty-backed MultiLSPy queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import os
9
+ from contextlib import AsyncExitStack, asynccontextmanager
10
+ from pathlib import Path
11
+ from shutil import which
12
+ from typing import Any, AsyncIterator, Awaitable, Callable, TypeVar
13
+ from urllib.parse import urlparse
14
+
15
+ from multilspy.language_server import LanguageServer
16
+ from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
17
+ from multilspy.multilspy_config import Language, MultilspyConfig
18
+ from multilspy.multilspy_exceptions import MultilspyException
19
+ from multilspy.multilspy_logger import MultilspyLogger
20
+
21
+ # REQUIRED: Adjust parents[] if the card is not stored at .fast-agent/agent-cards/.
22
+ _REPO_ROOT = Path(__file__).resolve().parents[2]
23
+
24
+ # RECOMMENDED: Narrow LSP access to the parts of the repo you actually want queried.
25
+ # Use {"."} to allow the entire repo.
26
+ _ALLOWED_DIRS = {"src", "tests", "spec"}
27
+ _ALLOWED_FILES: set[str] = set()
28
+
29
+ _server_lock = asyncio.Lock()
30
+ _server_stack: AsyncExitStack | None = None
31
+ _server: "TyServer" | None = None
32
+
33
+ _CONTENT_MODIFIED_RETRY_ATTEMPTS = 2
34
+ _CONTENT_MODIFIED_BASE_DELAY_SECONDS = 0.05
35
+
36
+ _ReturnT = TypeVar("_ReturnT")
37
+
38
+
39
+ class TyServer(LanguageServer):
40
+ """Language server wrapper for ty language server."""
41
+
42
+ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
43
+ ty_cmd = _resolve_ty_cmd()
44
+ super().__init__(
45
+ config,
46
+ logger,
47
+ repository_root_path,
48
+ ProcessLaunchInfo(cmd=ty_cmd, cwd=repository_root_path),
49
+ "python",
50
+ )
51
+ self.diagnostics: dict[str, list[dict[str, Any]]] = {}
52
+
53
+ def _get_initialize_params(self, repository_absolute_path: str) -> dict[str, Any]:
54
+ root_uri = Path(repository_absolute_path).as_uri()
55
+ return {
56
+ "processId": os.getpid(),
57
+ "rootPath": repository_absolute_path,
58
+ "rootUri": root_uri,
59
+ "workspaceFolders": [
60
+ {
61
+ "uri": root_uri,
62
+ "name": Path(repository_absolute_path).name,
63
+ }
64
+ ],
65
+ "capabilities": {
66
+ "workspace": {"workspaceFolders": True},
67
+ "textDocument": {"hover": {"contentFormat": ["markdown", "plaintext"]}},
68
+ },
69
+ }
70
+
71
+ @asynccontextmanager
72
+ async def start_server(self) -> "AsyncIterator[TyServer]":
73
+ async def do_nothing(params: Any) -> None:
74
+ return None
75
+
76
+ async def window_log_message(msg: Any) -> None:
77
+ self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
78
+
79
+ async def publish_diagnostics(params: dict[str, Any]) -> None:
80
+ uri = params.get("uri")
81
+ if not uri:
82
+ return
83
+ self.diagnostics[uri] = params.get("diagnostics", [])
84
+
85
+ self.server.on_notification("window/logMessage", window_log_message)
86
+ self.server.on_request("workspace/executeClientCommand", do_nothing)
87
+ self.server.on_notification("$/progress", do_nothing)
88
+ self.server.on_notification("textDocument/publishDiagnostics", publish_diagnostics)
89
+
90
+ async with super().start_server():
91
+ self.logger.log("Starting ty language server process", logging.INFO)
92
+ await self.server.start()
93
+ initialize_params = self._get_initialize_params(self.repository_root_path)
94
+ self.logger.log(
95
+ "Sending initialize request from LSP client to ty language server", logging.INFO
96
+ )
97
+ await self.server.send.initialize(initialize_params)
98
+ self.server.notify.initialized({})
99
+ yield self
100
+ await self.server.shutdown()
101
+ await self.server.stop()
102
+
103
+
104
+ def _resolve_ty_cmd() -> str:
105
+ executable = which("ty")
106
+ if executable is None:
107
+ raise MultilspyException("ty is not available on PATH. Install the 'ty' package.")
108
+ return f"{executable} server"
109
+
110
+
111
+ def _allow_all_paths() -> bool:
112
+ return "." in _ALLOWED_DIRS
113
+
114
+
115
+ def _allowed_path_error() -> str:
116
+ if _allow_all_paths():
117
+ return ""
118
+ if _ALLOWED_DIRS and _ALLOWED_FILES:
119
+ allowed_dirs = ", ".join(sorted(_ALLOWED_DIRS))
120
+ allowed_files = ", ".join(sorted(_ALLOWED_FILES))
121
+ return f"Path must live under one of: {allowed_dirs}; or be one of: {allowed_files}."
122
+ if _ALLOWED_DIRS:
123
+ allowed_dirs = ", ".join(sorted(_ALLOWED_DIRS))
124
+ return f"Path must live under {allowed_dirs}."
125
+ if _ALLOWED_FILES:
126
+ allowed_files = ", ".join(sorted(_ALLOWED_FILES))
127
+ return f"Path must be one of: {allowed_files}."
128
+ return "Path is not allowed. Configure _ALLOWED_DIRS or _ALLOWED_FILES."
129
+
130
+
131
+ def _path_is_allowed(relative_path: Path) -> bool:
132
+ if _allow_all_paths():
133
+ return True
134
+ if len(relative_path.parts) == 1:
135
+ return relative_path.name in _ALLOWED_FILES
136
+ return relative_path.parts[0] in _ALLOWED_DIRS
137
+
138
+
139
+ def _resolve_relative_path(file_path: str) -> str:
140
+ path = Path(file_path)
141
+ path = (_REPO_ROOT / path).resolve() if not path.is_absolute() else path.resolve()
142
+
143
+ try:
144
+ relative_path = path.relative_to(_REPO_ROOT)
145
+ except ValueError as exc: # pragma: no cover - defensive guard
146
+ raise ValueError("Path is outside the repository root.") from exc
147
+
148
+ if not relative_path.parts:
149
+ raise ValueError("Path must point to a file within the repository.")
150
+
151
+ if not _path_is_allowed(relative_path):
152
+ raise ValueError(_allowed_path_error())
153
+
154
+ if not path.exists():
155
+ raise ValueError(f"File not found: {path}")
156
+
157
+ return str(relative_path)
158
+
159
+
160
+ async def _ensure_server() -> TyServer:
161
+ global _server_stack, _server
162
+ if _server is not None and _server.server_started:
163
+ return _server
164
+
165
+ async with _server_lock:
166
+ if _server is not None and _server.server_started:
167
+ return _server
168
+
169
+ config = MultilspyConfig(code_language=Language.PYTHON)
170
+ logger = MultilspyLogger()
171
+ server = TyServer(config, logger, str(_REPO_ROOT))
172
+ stack = AsyncExitStack()
173
+ await stack.enter_async_context(server.start_server())
174
+ _server = server
175
+ _server_stack = stack
176
+ return server
177
+
178
+
179
+ def _format_range(range_data: dict[str, Any] | None) -> str:
180
+ if not range_data:
181
+ return ""
182
+ start = range_data.get("start", {})
183
+ line = start.get("line")
184
+ character = start.get("character")
185
+ if line is None or character is None:
186
+ return ""
187
+ return f"{line + 1}:{character + 1}"
188
+
189
+
190
+ def _uri_to_relative(uri: str | None) -> str:
191
+ if not uri:
192
+ return ""
193
+ if uri.startswith("file:"):
194
+ parsed = urlparse(uri)
195
+ path = Path(parsed.path)
196
+ try:
197
+ return str(path.relative_to(_REPO_ROOT))
198
+ except ValueError:
199
+ return str(path)
200
+ return uri
201
+
202
+
203
+ def _format_locations(locations: list[dict[str, Any]]) -> str:
204
+ if not locations:
205
+ return "No locations returned."
206
+
207
+ lines = ["| path | line |", "| --- | --- |"]
208
+ for location in locations:
209
+ path = (
210
+ location.get("relativePath")
211
+ or location.get("absolutePath")
212
+ or _uri_to_relative(location.get("uri"))
213
+ )
214
+ line = _format_range(location.get("range"))
215
+ lines.append(f"| {path} | {line} |")
216
+ return "\n".join(lines)
217
+
218
+
219
+ def _format_hover_contents(contents: Any) -> str:
220
+ if contents is None:
221
+ return "No hover contents returned."
222
+ if isinstance(contents, str):
223
+ return contents
224
+ if isinstance(contents, list):
225
+ return "\n\n".join(_format_hover_contents(item) for item in contents)
226
+ if isinstance(contents, dict):
227
+ value = contents.get("value")
228
+ if isinstance(value, str):
229
+ return value
230
+ return json.dumps(contents, indent=2)
231
+ return str(contents)
232
+
233
+
234
+ def _format_symbol_kind(kind: Any) -> str:
235
+ if not isinstance(kind, int):
236
+ return str(kind or "")
237
+ return {
238
+ 1: "File",
239
+ 2: "Module",
240
+ 3: "Namespace",
241
+ 4: "Package",
242
+ 5: "Class",
243
+ 6: "Method",
244
+ 7: "Property",
245
+ 8: "Field",
246
+ 9: "Constructor",
247
+ 10: "Enum",
248
+ 11: "Interface",
249
+ 12: "Function",
250
+ 13: "Variable",
251
+ 14: "Constant",
252
+ 15: "String",
253
+ 16: "Number",
254
+ 17: "Boolean",
255
+ 18: "Array",
256
+ 19: "Object",
257
+ 20: "Key",
258
+ 21: "Null",
259
+ 22: "EnumMember",
260
+ 23: "Struct",
261
+ 24: "Event",
262
+ 25: "Operator",
263
+ 26: "TypeParameter",
264
+ }.get(kind, str(kind))
265
+
266
+
267
+ def _format_symbols(symbols: list[dict[str, Any]], default_path: str | None = None) -> str:
268
+ if not symbols:
269
+ return "No symbols returned."
270
+ lines = ["| name | kind | location | detail |", "| --- | --- | --- | --- |"]
271
+ for symbol in symbols:
272
+ location = symbol.get("location") or {}
273
+ path = (
274
+ location.get("relativePath")
275
+ or location.get("absolutePath")
276
+ or _uri_to_relative(location.get("uri"))
277
+ or ""
278
+ )
279
+ if not path and default_path:
280
+ path = default_path
281
+ range_data = location.get("range") or symbol.get("range") or symbol.get("selectionRange")
282
+ line = _format_range(range_data)
283
+ location_display = f"{path} ({line})" if path and line else path
284
+ lines.append(
285
+ "| {name} | {kind} | {location} | {detail} |".format(
286
+ name=symbol.get("name", ""),
287
+ kind=_format_symbol_kind(symbol.get("kind")),
288
+ location=location_display,
289
+ detail=symbol.get("detail", "") or "",
290
+ )
291
+ )
292
+ return "\n".join(lines)
293
+
294
+
295
+ def _is_content_modified_error(exc: Exception) -> bool:
296
+ message = str(exc).lower()
297
+ return "content modified" in message or "-32801" in message
298
+
299
+
300
+ async def _retry_on_content_modified(operation: Callable[[], Awaitable[_ReturnT]]) -> _ReturnT:
301
+ for attempt in range(_CONTENT_MODIFIED_RETRY_ATTEMPTS + 1):
302
+ try:
303
+ return await operation()
304
+ except asyncio.CancelledError:
305
+ raise
306
+ except Exception as exc:
307
+ if attempt == _CONTENT_MODIFIED_RETRY_ATTEMPTS or not _is_content_modified_error(exc):
308
+ raise
309
+ await asyncio.sleep(_CONTENT_MODIFIED_BASE_DELAY_SECONDS * (2**attempt))
310
+ raise RuntimeError("Retry loop exhausted unexpectedly.")
311
+
312
+
313
+ async def lsp_hover(file_path: str, line: int, character: int) -> str:
314
+ """Return hover information for a symbol at the given location."""
315
+ try:
316
+ relative_path = _resolve_relative_path(file_path)
317
+ server = await _ensure_server()
318
+ hover = await _retry_on_content_modified(
319
+ lambda: server.request_hover(relative_path, line, character)
320
+ )
321
+ if not hover:
322
+ return "No hover information returned."
323
+ return _format_hover_contents(hover.get("contents"))
324
+ except (ValueError, MultilspyException) as exc:
325
+ return f"Error: {exc}"
326
+ except Exception as exc: # pragma: no cover - defensive guard
327
+ return f"Error: {exc}"
328
+
329
+
330
+ async def lsp_definition(file_path: str, line: int, character: int) -> str:
331
+ """Return definition locations for a symbol at the given location."""
332
+ try:
333
+ relative_path = _resolve_relative_path(file_path)
334
+ server = await _ensure_server()
335
+ locations = await _retry_on_content_modified(
336
+ lambda: server.request_definition(relative_path, line, character)
337
+ )
338
+ if not locations:
339
+ return "No locations returned."
340
+ return _format_locations([dict(location) for location in locations])
341
+ except (ValueError, MultilspyException) as exc:
342
+ message = str(exc)
343
+ if "Unexpected response from Language Server" in message:
344
+ return "No locations returned."
345
+ return f"Error: {exc}"
346
+ except Exception as exc: # pragma: no cover - defensive guard
347
+ message = str(exc)
348
+ if "Unexpected response from Language Server" in message:
349
+ return "No locations returned."
350
+ return f"Error: {exc}"
351
+
352
+
353
+ async def lsp_references(file_path: str, line: int, character: int) -> str:
354
+ """Return reference locations for a symbol at the given location."""
355
+ try:
356
+ relative_path = _resolve_relative_path(file_path)
357
+ server = await _ensure_server()
358
+ locations = await _retry_on_content_modified(
359
+ lambda: server.request_references(relative_path, line, character)
360
+ )
361
+ if not locations:
362
+ return "No locations returned."
363
+ return _format_locations([dict(location) for location in locations])
364
+ except (ValueError, MultilspyException) as exc:
365
+ message = str(exc)
366
+ if "Unexpected response from Language Server" in message:
367
+ return "No locations returned."
368
+ return f"Error: {exc}"
369
+ except Exception as exc: # pragma: no cover - defensive guard
370
+ message = str(exc)
371
+ if "Unexpected response from Language Server" in message:
372
+ return "No locations returned."
373
+ return f"Error: {exc}"
374
+
375
+
376
+ async def lsp_document_symbols(file_path: str) -> str:
377
+ """Return document symbols for a file."""
378
+ try:
379
+ relative_path = _resolve_relative_path(file_path)
380
+ server = await _ensure_server()
381
+ symbols, _ = await _retry_on_content_modified(
382
+ lambda: server.request_document_symbols(relative_path)
383
+ )
384
+ return _format_symbols([dict(symbol) for symbol in symbols], default_path=relative_path)
385
+ except (ValueError, MultilspyException) as exc:
386
+ return f"Error: {exc}"
387
+ except Exception as exc: # pragma: no cover - defensive guard
388
+ return f"Error: {exc}"
389
+
390
+
391
+ async def lsp_workspace_symbols(query: str) -> str:
392
+ """Return workspace symbols matching a query string."""
393
+ try:
394
+ server = await _ensure_server()
395
+ symbols = await _retry_on_content_modified(lambda: server.request_workspace_symbol(query))
396
+ if symbols is None:
397
+ return "No symbols returned."
398
+ return _format_symbols([dict(symbol) for symbol in symbols])
399
+ except (ValueError, MultilspyException) as exc:
400
+ return f"Error: {exc}"
401
+ except Exception as exc: # pragma: no cover - defensive guard
402
+ return f"Error: {exc}"
403
+
404
+
405
+ async def lsp_diagnostics(file_path: str | None = None) -> str:
406
+ """Return cached diagnostics from ty server."""
407
+ try:
408
+ server = await _ensure_server()
409
+ if file_path is None:
410
+ diagnostics = server.diagnostics
411
+ else:
412
+ relative_path = _resolve_relative_path(file_path)
413
+ uri = Path(_REPO_ROOT / relative_path).as_uri()
414
+ diagnostics = {uri: server.diagnostics.get(uri, [])}
415
+ if not diagnostics:
416
+ return "No diagnostics cached."
417
+ return json.dumps(diagnostics, indent=2)
418
+ except (ValueError, MultilspyException) as exc:
419
+ return f"Error: {exc}"
420
+ except Exception as exc: # pragma: no cover - defensive guard
421
+ return f"Error: {exc}"
@@ -0,0 +1,3 @@
1
+ model_references:
2
+ system:
3
+ last_used: codexresponses.gpt-5.5?reasoning=medium