llm-codegen-research 2.10__tar.gz → 2.12__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 (44) hide show
  1. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/PKG-INFO +1 -1
  2. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/pyproject.toml +6 -0
  3. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/analyse/languages/__init__.py +3 -0
  4. llm_codegen_research-2.12/src/llm_cgr/analyse/languages/rust.py +193 -0
  5. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/clients/anthropic.py +8 -6
  6. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/clients/deepseek.py +5 -3
  7. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/clients/mistral.py +4 -4
  8. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/clients/nscale.py +5 -3
  9. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/clients/together.py +9 -6
  10. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_codegen_research.egg-info/PKG-INFO +1 -1
  11. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_codegen_research.egg-info/SOURCES.txt +1 -0
  12. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/tests/test_enums.py +3 -2
  13. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/tests/test_llm_api.py +6 -0
  14. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/LICENSE +0 -0
  15. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/README.md +0 -0
  16. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/setup.cfg +0 -0
  17. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/__init__.py +0 -0
  18. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/analyse/__init__.py +0 -0
  19. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/analyse/classes.py +0 -0
  20. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/analyse/languages/code_data.py +0 -0
  21. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/analyse/languages/javascript.py +0 -0
  22. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/analyse/languages/python.py +0 -0
  23. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/analyse/regexes.py +0 -0
  24. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/decorators.py +0 -0
  25. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/defaults.py +0 -0
  26. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/enums.py +0 -0
  27. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/json_utils.py +0 -0
  28. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/__init__.py +0 -0
  29. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/clients/__init__.py +0 -0
  30. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/clients/base.py +0 -0
  31. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/clients/openai.py +0 -0
  32. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/clients/protocol.py +0 -0
  33. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/generate.py +0 -0
  34. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/llm/prompts.py +0 -0
  35. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/py.typed +0 -0
  36. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/scripts/test_cuda.py +0 -0
  37. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_cgr/timeout.py +0 -0
  38. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_codegen_research.egg-info/dependency_links.txt +0 -0
  39. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_codegen_research.egg-info/entry_points.txt +0 -0
  40. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_codegen_research.egg-info/requires.txt +0 -0
  41. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/src/llm_codegen_research.egg-info/top_level.txt +0 -0
  42. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/tests/test_json_utils.py +0 -0
  43. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/tests/test_llm_local.py +0 -0
  44. {llm_codegen_research-2.10 → llm_codegen_research-2.12}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llm-codegen-research
3
- Version: 2.10
3
+ Version: 2.12
4
4
  Summary: Useful classes and methods for researching code-generation by LLMs.
5
5
  Author-email: Lukas Twist <itsluketwist@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/itsluketwist/llm-codegen-research
@@ -46,5 +46,11 @@ dev = [
46
46
  "uv",
47
47
  ]
48
48
 
49
+ [tool.pytest.ini_options]
50
+ markers = [
51
+ # tests that make real external api calls - excluded from ci runs
52
+ "api: marks tests as making external api calls",
53
+ ]
54
+
49
55
  [project.scripts]
50
56
  test_cuda = "llm_cgr.scripts.test_cuda:main"
@@ -1,6 +1,7 @@
1
1
  from llm_cgr.analyse.languages.code_data import CodeData
2
2
  from llm_cgr.analyse.languages.javascript import analyse_javascript_code
3
3
  from llm_cgr.analyse.languages.python import analyse_python_code
4
+ from llm_cgr.analyse.languages.rust import analyse_rust_code
4
5
 
5
6
 
6
7
  def analyse_code(code: str, language: str | None) -> CodeData:
@@ -12,6 +13,8 @@ def analyse_code(code: str, language: str | None) -> CodeData:
12
13
  return analyse_python_code(code=code)
13
14
  elif language == "javascript":
14
15
  return analyse_javascript_code(code=code)
16
+ elif language == "rust":
17
+ return analyse_rust_code(code=code)
15
18
 
16
19
  except Exception as exc:
17
20
  return CodeData(
@@ -0,0 +1,193 @@
1
+ """Utility functions for Rust code analysis."""
2
+
3
+ import re
4
+
5
+ from llm_cgr.analyse.languages.code_data import CodeData
6
+
7
+
8
+ # rust's three standard library crates - everything else comes from crates.io
9
+ RUST_STDLIB = frozenset({"std", "core", "alloc"})
10
+
11
+ # matches extern crate declarations: extern crate serde;
12
+ _EXTERN_CRATE_RE = re.compile(r"^\s*extern\s+crate\s+(\w+)", re.MULTILINE)
13
+
14
+
15
+ def _strip_comments(code: str) -> str:
16
+ """Remove // line comments and /* */ block comments from code."""
17
+ # remove block comments first (they can span multiple lines)
18
+ code = re.sub(r"/\*.*?\*/", "", code, flags=re.DOTALL)
19
+ # remove line comments
20
+ code = re.sub(r"//[^\n]*", "", code)
21
+ return code
22
+
23
+
24
+ def _extract_use_statements(code: str) -> list[str]:
25
+ """
26
+ Extract raw use paths from all use declarations in the code.
27
+
28
+ Uses a bracket-aware scanner rather than plain regex, because braced imports
29
+ like `use std::{io, fmt};` span multiple tokens and can be multi-line.
30
+ Returns a list of raw path strings, e.g. ["std::collections::HashMap", "tokio"].
31
+ """
32
+ paths = []
33
+ i = 0
34
+ while i < len(code):
35
+ # find the next 'use' keyword (word boundary ensures we skip 'reuse', etc.)
36
+ match = re.search(r"\buse\s+", code[i:])
37
+ if match is None:
38
+ break
39
+
40
+ start = i + match.end()
41
+ depth = 0
42
+ j = start
43
+
44
+ # scan forward until we hit a ';' at brace-depth 0
45
+ while j < len(code):
46
+ c = code[j]
47
+ if c == "{":
48
+ depth += 1
49
+ elif c == "}":
50
+ depth -= 1
51
+ elif c == ";" and depth == 0:
52
+ paths.append(code[start:j].strip())
53
+ break
54
+ j += 1
55
+
56
+ i = start # advance past the 'use' keyword we just processed
57
+
58
+ return paths
59
+
60
+
61
+ def _split_top_level(s: str) -> list[str]:
62
+ """
63
+ Split a comma-separated string into items, respecting brace nesting.
64
+
65
+ Used to split the contents of {...} groups in use paths.
66
+ """
67
+ items = []
68
+ depth = 0
69
+ current: list[str] = []
70
+
71
+ for c in s:
72
+ if c == "{":
73
+ depth += 1
74
+ current.append(c)
75
+ elif c == "}":
76
+ depth -= 1
77
+ current.append(c)
78
+ elif c == "," and depth == 0:
79
+ items.append("".join(current).strip())
80
+ current = []
81
+ else:
82
+ current.append(c)
83
+
84
+ if current:
85
+ items.append("".join(current).strip())
86
+
87
+ return items
88
+
89
+
90
+ def _expand_use_path(path: str, prefix: str = "") -> list[str]:
91
+ """
92
+ Recursively expand a Rust use path into fully-qualified dotted import strings.
93
+
94
+ Handles simple paths, braced groups, wildcards, and 'as' aliases.
95
+ For example:
96
+ "std::collections::HashMap" -> ["std.collections.HashMap"]
97
+ "std::{io::Read, fmt}" -> ["std.io.Read", "std.fmt"]
98
+ "std::io::{self, Write}" -> ["std.io", "std.io.Write"]
99
+ Returns a list of dotted import paths.
100
+ """
101
+ # strip any trailing 'as alias' at this level of the path
102
+ path = re.sub(r"\s+as\s+\w+\s*$", "", path).strip()
103
+
104
+ if not path:
105
+ return []
106
+
107
+ # handle wildcard: use std::collections::*;
108
+ if path.endswith("::*"):
109
+ prefix_part = path[:-3].replace("::", ".")
110
+ full = f"{prefix}.{prefix_part}" if prefix else prefix_part
111
+ return [f"{full}.*"]
112
+
113
+ brace_idx = path.find("{")
114
+
115
+ if brace_idx == -1:
116
+ # simple path, no braces: std::collections::HashMap
117
+ converted = path.replace("::", ".")
118
+ full = f"{prefix}.{converted}" if prefix else converted
119
+ return [full]
120
+
121
+ # braced path: std::{io::Read, fmt} or std::io::{self, Write}
122
+ # everything before the brace is the common prefix
123
+ before_brace = path[:brace_idx].rstrip(":").replace("::", ".")
124
+ new_prefix = (
125
+ f"{prefix}.{before_brace}"
126
+ if (prefix and before_brace)
127
+ else (prefix or before_brace)
128
+ )
129
+
130
+ # extract the content inside the outermost braces
131
+ close_idx = path.rfind("}")
132
+ inner = path[brace_idx + 1 : close_idx]
133
+
134
+ results = []
135
+ for item in _split_top_level(inner):
136
+ item = item.strip()
137
+ if not item:
138
+ continue
139
+
140
+ if item == "self":
141
+ # 'self' refers to the module itself (the prefix path)
142
+ results.append(new_prefix)
143
+ else:
144
+ results.extend(_expand_use_path(item, prefix=new_prefix))
145
+
146
+ return results
147
+
148
+
149
+ def analyse_rust_code(code: str) -> CodeData:
150
+ """
151
+ Analyse Rust code to extract imported crates and their paths.
152
+
153
+ Only extracts use and extern crate declarations; no usage tracking or syntax
154
+ validation is performed (valid is always True).
155
+ Returns a CodeData object with import information.
156
+ """
157
+ std_libs: set[str] = set()
158
+ ext_libs: set[str] = set()
159
+ imports: set[str] = set()
160
+
161
+ # strip comments so 'use' inside comments isn't matched
162
+ clean_code = _strip_comments(code)
163
+
164
+ # process each use declaration
165
+ for raw_path in _extract_use_statements(clean_code):
166
+ for dotted_path in _expand_use_path(raw_path):
167
+ imports.add(dotted_path)
168
+ # the top-level segment is the crate name
169
+ top_level = dotted_path.split(".")[0]
170
+ if top_level in RUST_STDLIB:
171
+ std_libs.add(top_level)
172
+ else:
173
+ ext_libs.add(top_level)
174
+
175
+ # handle extern crate declarations (older rust style, still used occasionally)
176
+ for match in _EXTERN_CRATE_RE.finditer(clean_code):
177
+ crate_name = match.group(1)
178
+ # 'self' and 'std' as extern crate are special cases, skip them
179
+ if crate_name in ("self", "super"):
180
+ continue
181
+ imports.add(crate_name)
182
+ if crate_name in RUST_STDLIB:
183
+ std_libs.add(crate_name)
184
+ else:
185
+ ext_libs.add(crate_name)
186
+
187
+ return CodeData(
188
+ valid=True,
189
+ std_libs=std_libs,
190
+ ext_libs=ext_libs,
191
+ imports=imports,
192
+ lib_usage={},
193
+ )
@@ -1,8 +1,9 @@
1
1
  """Classes to access LLMs via the Anthropic Claude API."""
2
2
 
3
- from typing import Any
3
+ from typing import Any, cast
4
4
 
5
5
  import anthropic
6
+ from anthropic.types import MessageParam, TextBlock
6
7
 
7
8
  from llm_cgr.defaults import DEFAULT_MAX_TOKENS
8
9
  from llm_cgr.llm.clients.base import Base_LLM
@@ -69,10 +70,11 @@ class Anthropic_LLM(Base_LLM):
69
70
  """Generate a model response from the Anthropic API."""
70
71
  response = self._client.messages.create(
71
72
  model=model,
72
- system=system or self._system or anthropic.NOT_GIVEN,
73
- messages=input,
74
- temperature=temperature if temperature is not None else anthropic.NOT_GIVEN,
75
- top_p=top_p if top_p is not None else anthropic.NOT_GIVEN,
73
+ system=system or self._system or anthropic.omit,
74
+ messages=cast(list[MessageParam], input),
75
+ temperature=temperature if temperature is not None else anthropic.omit,
76
+ top_p=top_p if top_p is not None else anthropic.omit,
76
77
  max_tokens=max_tokens if max_tokens is not None else DEFAULT_MAX_TOKENS,
77
78
  )
78
- return response.content[0].text
79
+ # cast to TextBlock as non-tool, non-thinking requests always return text
80
+ return cast(TextBlock, response.content[0]).text
@@ -1,9 +1,10 @@
1
1
  """Class to access LLMs via the OpenAI API."""
2
2
 
3
3
  import os
4
- from typing import Any
4
+ from typing import Any, cast
5
5
 
6
6
  import openai
7
+ from openai.types.chat import ChatCompletionMessageParam
7
8
 
8
9
  from llm_cgr.llm.clients.base import Base_LLM
9
10
 
@@ -67,10 +68,11 @@ class DeepSeek_LLM(Base_LLM):
67
68
  ) -> str:
68
69
  """Generate a model response from the OpenAI API."""
69
70
  response = self._client.chat.completions.create(
70
- messages=input,
71
+ messages=cast(list[ChatCompletionMessageParam], input),
71
72
  model=model,
72
73
  temperature=temperature if temperature is not None else openai.omit,
73
74
  top_p=top_p if top_p is not None else openai.omit,
74
75
  max_completion_tokens=max_tokens if max_tokens is not None else openai.omit,
75
76
  )
76
- return response.choices[0].message.content
77
+ # cast to str as text completions always return string content
78
+ return cast(str, response.choices[0].message.content)
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  from typing import Any
5
5
 
6
- import mistralai
6
+ from mistralai import client
7
7
 
8
8
  from llm_cgr.llm.clients.base import Base_LLM
9
9
 
@@ -31,7 +31,7 @@ class Mistral_LLM(Base_LLM):
31
31
  top_p=top_p,
32
32
  max_tokens=max_tokens,
33
33
  )
34
- self._client = mistralai.Mistral(
34
+ self._client = client.Mistral(
35
35
  api_key=os.environ["MISTRAL_API_KEY"],
36
36
  )
37
37
 
@@ -71,8 +71,8 @@ class Mistral_LLM(Base_LLM):
71
71
  response = self._client.chat.complete(
72
72
  model=model,
73
73
  messages=input,
74
- temperature=temperature if temperature is not None else mistralai.UNSET,
74
+ temperature=temperature if temperature is not None else client.UNSET,
75
75
  top_p=top_p,
76
- max_tokens=max_tokens if max_tokens is not None else mistralai.UNSET,
76
+ max_tokens=max_tokens if max_tokens is not None else client.UNSET,
77
77
  )
78
78
  return response.choices[0].message.content
@@ -1,9 +1,10 @@
1
1
  """Class to access LLMs via the OpenAI API."""
2
2
 
3
3
  import os
4
- from typing import Any
4
+ from typing import Any, cast
5
5
 
6
6
  import openai
7
+ from openai.types.chat import ChatCompletionMessageParam
7
8
 
8
9
  from llm_cgr.llm.clients.base import Base_LLM
9
10
 
@@ -67,10 +68,11 @@ class Nscale_LLM(Base_LLM):
67
68
  ) -> str:
68
69
  """Generate a model response from the OpenAI API."""
69
70
  response = self._client.chat.completions.create(
70
- messages=input,
71
+ messages=cast(list[ChatCompletionMessageParam], input),
71
72
  model=model,
72
73
  temperature=temperature if temperature is not None else openai.omit,
73
74
  top_p=top_p if top_p is not None else openai.omit,
74
75
  max_completion_tokens=max_tokens if max_tokens is not None else openai.omit,
75
76
  )
76
- return response.choices[0].message.content
77
+ # cast to str as text completions always return string content
78
+ return cast(str, response.choices[0].message.content)
@@ -1,6 +1,6 @@
1
1
  """Class to access LLMs via the TogetherAI API."""
2
2
 
3
- from typing import Any
3
+ from typing import Any, cast
4
4
 
5
5
  import together
6
6
 
@@ -64,9 +64,12 @@ class TogetherAI_LLM(Base_LLM):
64
64
  """Generate a model response from the TogetherAI API."""
65
65
  response = self._client.chat.completions.create(
66
66
  model=model,
67
- messages=input,
68
- temperature=temperature,
69
- top_p=top_p,
70
- max_tokens=max_tokens,
67
+ messages=cast(Any, input),
68
+ temperature=temperature if temperature is not None else together.omit,
69
+ top_p=top_p if top_p is not None else together.omit,
70
+ max_tokens=max_tokens if max_tokens is not None else together.omit,
71
71
  )
72
- return response.choices[0].message.content
72
+ # cast to Any first as together doesn't publicly export the message type,
73
+ # then cast content to str as text completions always have it set
74
+ message = cast(Any, response.choices[0].message)
75
+ return cast(str, message.content)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llm-codegen-research
3
- Version: 2.10
3
+ Version: 2.12
4
4
  Summary: Useful classes and methods for researching code-generation by LLMs.
5
5
  Author-email: Lukas Twist <itsluketwist@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/itsluketwist/llm-codegen-research
@@ -15,6 +15,7 @@ src/llm_cgr/analyse/languages/__init__.py
15
15
  src/llm_cgr/analyse/languages/code_data.py
16
16
  src/llm_cgr/analyse/languages/javascript.py
17
17
  src/llm_cgr/analyse/languages/python.py
18
+ src/llm_cgr/analyse/languages/rust.py
18
19
  src/llm_cgr/llm/__init__.py
19
20
  src/llm_cgr/llm/generate.py
20
21
  src/llm_cgr/llm/prompts.py
@@ -1,4 +1,5 @@
1
1
  from enum import auto
2
+ from typing import Any
2
3
 
3
4
  from llm_cgr import OptionsEnum
4
5
 
@@ -26,8 +27,8 @@ def test_options_enum():
26
27
  assert (TestEnum.ONE != "One") is False
27
28
  assert (TestEnum.ONE != "ONE") is False
28
29
 
29
- # check that the enum can be hashed
30
- test_dict = {TestEnum.TWO: "value"}
30
+ # check that the enum can be hashed and string keys match enum keys at runtime
31
+ test_dict: dict[Any, str] = {TestEnum.TWO: "value"}
31
32
  assert test_dict[TestEnum.TWO] == "value"
32
33
  assert test_dict["two"] == "value"
33
34
  assert {TestEnum.THREE} == {"three"}
@@ -1,8 +1,14 @@
1
1
  """Test our connection and usage of the LLM APIs."""
2
2
 
3
+ import pytest
4
+
3
5
  from llm_cgr import BASE_SYSTEM_PROMPT, generate, generate_bool, generate_list, get_llm
4
6
 
5
7
 
8
+ # mark all tests in this file as api tests, so they can be excluded in ci
9
+ pytestmark = pytest.mark.api
10
+
11
+
6
12
  def test_generate(model):
7
13
  """
8
14
  Test the generate method.