Coverage for little_loops / learning_tests / extractor.py: 0%

38 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-26 17:38 -0500

1"""LLM-based extraction of external API dependencies from issue text (ENH-2209). 

2 

3Exposes ``extract_learning_targets()`` as an importable callable Python module 

4so downstream tools (ENH-2210 sprint pre-flight) can import it directly without 

5a shell-out. The Anthropic SDK is used for SDK-direct invocation; the ``llm_call`` 

6parameter allows mock injection for unit tests. 

7 

8Follow the same pattern as ``gate.py`` (``is_record_stale``) — importable helper, 

9unit-testable with mock injection. 

10""" 

11 

12from __future__ import annotations 

13 

14import json 

15import re 

16from collections.abc import Callable 

17 

18from little_loops.issue_parser import slugify 

19 

20_EXTRACTION_PROMPT = """\ 

21Analyze the following issue text and identify all external packages, SDKs, or \ 

22third-party API surfaces that the implementation plan assumes behavior of. 

23 

24Include: 

25- Third-party Python packages (e.g. anthropic, requests, boto3, stripe) 

26- External APIs and services (e.g. Stripe webhooks, GitHub API) 

27- SDKs for external platforms or cloud services 

28- Non-obvious stdlib components whose contract is non-trivial (e.g. asyncio, multiprocessing) 

29 

30Exclude: 

31- Code internal to the project being built 

32- Standard Python builtins (str, dict, list, int, etc.) 

33- Contract-stable stdlib modules (os, sys, pathlib, json, re, datetime) 

34 

35For each identified dependency, provide its canonical short name only \ 

36(no version qualifier, no description). 

37 

38Return a JSON object as the LAST line of your response in exactly this format: 

39TARGETS_JSON:{{"targets": ["name1", "name2"], "count": N}} 

40 

41If there are no external dependencies, return: 

42TARGETS_JSON:{{"targets": [], "count": 0}} 

43 

44Issue text to analyze: 

45--- 

46{issue_text} 

47---\ 

48""" 

49 

50_TARGETS_JSON_RE = re.compile(r"TARGETS_JSON:(\{.*\})", re.MULTILINE) 

51 

52 

53def _default_llm_call(prompt: str) -> str: 

54 """Call the Anthropic API via the SDK and return the response text.""" 

55 import anthropic 

56 

57 client = anthropic.Anthropic() 

58 message = client.messages.create( 

59 model="claude-haiku-4-5-20251001", 

60 max_tokens=1024, 

61 messages=[{"role": "user", "content": prompt}], 

62 ) 

63 block = message.content[0] 

64 if not isinstance(block, anthropic.types.TextBlock): 

65 return "" 

66 return block.text 

67 

68 

69def extract_learning_targets( 

70 issue_text: str, 

71 *, 

72 llm_call: Callable[[str], str] | None = None, 

73) -> list[str]: 

74 """Extract external API dependency names from issue text via LLM. 

75 

76 Returns a deduplicated list of target names. Issues with no external 

77 dependencies return an empty list; callers should omit the frontmatter 

78 field rather than writing an empty list. 

79 

80 Args: 

81 issue_text: Full issue file content (frontmatter + body). 

82 llm_call: Optional callable accepting a prompt string and returning 

83 response text. Defaults to SDK-direct Anthropic call. 

84 Inject a mock for unit tests. 

85 

86 Returns: 

87 Deduplicated list of target names (first-seen form preserved), 

88 e.g. ``["anthropic", "requests"]``. 

89 """ 

90 caller = llm_call if llm_call is not None else _default_llm_call 

91 prompt = _EXTRACTION_PROMPT.format(issue_text=issue_text) 

92 response = caller(prompt) 

93 

94 match = _TARGETS_JSON_RE.search(response) 

95 if not match: 

96 return [] 

97 

98 try: 

99 data = json.loads(match.group(1)) 

100 except (json.JSONDecodeError, KeyError): 

101 return [] 

102 

103 raw_targets: list[str] = data.get("targets") or [] 

104 seen: set[str] = set() 

105 result: list[str] = [] 

106 for t in raw_targets: 

107 name = t.strip() 

108 if not name: 

109 continue 

110 slug = slugify(name) 

111 if slug not in seen: 

112 seen.add(slug) 

113 result.append(name) 

114 return result