llm-codegen-research 1.2__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 (27) hide show
  1. llm_codegen_research-1.2/LICENSE +21 -0
  2. llm_codegen_research-1.2/PKG-INFO +101 -0
  3. llm_codegen_research-1.2/README.md +84 -0
  4. llm_codegen_research-1.2/pyproject.toml +39 -0
  5. llm_codegen_research-1.2/setup.cfg +4 -0
  6. llm_codegen_research-1.2/src/llm_cgr/__init__.py +19 -0
  7. llm_codegen_research-1.2/src/llm_cgr/analyse/__init__.py +10 -0
  8. llm_codegen_research-1.2/src/llm_cgr/analyse/classes.py +139 -0
  9. llm_codegen_research-1.2/src/llm_cgr/analyse/languages/__init__.py +24 -0
  10. llm_codegen_research-1.2/src/llm_cgr/analyse/languages/code_data.py +34 -0
  11. llm_codegen_research-1.2/src/llm_cgr/analyse/languages/python.py +86 -0
  12. llm_codegen_research-1.2/src/llm_cgr/analyse/regexes.py +10 -0
  13. llm_codegen_research-1.2/src/llm_cgr/defaults.py +10 -0
  14. llm_codegen_research-1.2/src/llm_cgr/json_utils.py +24 -0
  15. llm_codegen_research-1.2/src/llm_cgr/py.typed +0 -0
  16. llm_codegen_research-1.2/src/llm_cgr/query/__init__.py +18 -0
  17. llm_codegen_research-1.2/src/llm_cgr/query/api_utils.py +99 -0
  18. llm_codegen_research-1.2/src/llm_cgr/query/generate.py +149 -0
  19. llm_codegen_research-1.2/src/llm_cgr/query/prompts.py +13 -0
  20. llm_codegen_research-1.2/src/llm_cgr/query/protocol.py +20 -0
  21. llm_codegen_research-1.2/src/llm_codegen_research.egg-info/PKG-INFO +101 -0
  22. llm_codegen_research-1.2/src/llm_codegen_research.egg-info/SOURCES.txt +25 -0
  23. llm_codegen_research-1.2/src/llm_codegen_research.egg-info/dependency_links.txt +1 -0
  24. llm_codegen_research-1.2/src/llm_codegen_research.egg-info/requires.txt +3 -0
  25. llm_codegen_research-1.2/src/llm_codegen_research.egg-info/top_level.txt +1 -0
  26. llm_codegen_research-1.2/tests/test_analyse.py +125 -0
  27. llm_codegen_research-1.2/tests/test_query.py +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Luke Twist
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm-codegen-research
3
+ Version: 1.2
4
+ Summary: Utilities for use when research code-generation by LLMs.
5
+ Author-email: Lukas Twist <itsluketwist@gmail.com>
6
+ Project-URL: Homepage, https://github.com/itsluketwist/llm-codegen-research
7
+ Keywords: llm,code-generation,research,prompting,nlp
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: anthropic>=0.49.0
14
+ Requires-Dist: openai>=1.78.1
15
+ Requires-Dist: together>=1.5.8
16
+ Dynamic: license-file
17
+
18
+ # **llm-codegen-research**
19
+
20
+
21
+ ![lint code workflow](https://github.com/itsluketwist/llm-codegen-research/actions/workflows/lint.yaml/badge.svg)
22
+ ![test code workflow](https://github.com/itsluketwist/llm-codegen-research/actions/workflows/test.yaml/badge.svg)
23
+ ![release workflow](https://github.com/itsluketwist/llm-codegen-research/actions/workflows/release.yaml/badge.svg)
24
+
25
+
26
+ <div>
27
+ <!-- badges from : https://shields.io/ -->
28
+ <!-- logos available : https://simpleicons.org/ -->
29
+ <a href="https://opensource.org/licenses/MIT">
30
+ <img alt="MIT License" src="https://img.shields.io/badge/Licence-MIT-yellow?style=for-the-badge&logo=docs&logoColor=white" />
31
+ </a>
32
+ <a href="https://www.python.org/">
33
+ <img alt="Python 3" src="https://img.shields.io/badge/Python_3-blue?style=for-the-badge&logo=python&logoColor=white" />
34
+ </a>
35
+ <a href="https://openai.com/blog/openai-api/">
36
+ <img alt="OpenAI API" src="https://img.shields.io/badge/OpenAI_API-412991?style=for-the-badge&logo=openai&logoColor=white" />
37
+ </a>
38
+ <a href="https://www.anthropic.com/api/">
39
+ <img alt="Anthropic API" src="https://img.shields.io/badge/Claude_API-D97757?style=for-the-badge&logo=claude&logoColor=white" />
40
+ </a>
41
+ <a href="https://api.together.ai/">
42
+ <img alt="together.ai API" src="https://img.shields.io/badge/together.ai_API-B5B5B5?style=for-the-badge&logoColor=white" />
43
+ </a>
44
+ </div>
45
+
46
+
47
+ ## *usage*
48
+
49
+ A collection of methods and classes I repeatedly use when conducting research on LLM code-generation.
50
+ Covers both prompting various LLMs, and analysing the markdown responses.
51
+
52
+ ```python
53
+ from llm_cgr import quick_complete, Markdown
54
+
55
+ response = quick_complete("Write python code to generate the nth fibonacci number.")
56
+
57
+ markdown = Markdown(text=response)
58
+ ```
59
+
60
+ ## *installation*
61
+
62
+ Install directly from PyPI, using pip:
63
+
64
+ ```shell
65
+ pip install llm-codegen-research
66
+ ```
67
+
68
+ ## *development*
69
+
70
+ Clone the repository code:
71
+
72
+ ```shell
73
+ git clone https://github.com/itsluketwist/llm-codegen-research.git
74
+ ```
75
+
76
+ We use [`uv`](https://astral.sh/blog/uv) for project management.
77
+ Once cloned, create a virtual environment and install uv and the project:
78
+
79
+ ```shell
80
+ python -m venv .venv
81
+
82
+ . .venv/bin/activate
83
+
84
+ pip install uv
85
+
86
+ uv sync
87
+ ```
88
+
89
+ Use `make` commands to lint and test:
90
+
91
+ ```shell
92
+ make lint
93
+
94
+ make test
95
+ ```
96
+
97
+ Use `uv` to add new dependencies into the project and `uv.lock`:
98
+
99
+ ```shell
100
+ uv add openai
101
+ ```
@@ -0,0 +1,84 @@
1
+ # **llm-codegen-research**
2
+
3
+
4
+ ![lint code workflow](https://github.com/itsluketwist/llm-codegen-research/actions/workflows/lint.yaml/badge.svg)
5
+ ![test code workflow](https://github.com/itsluketwist/llm-codegen-research/actions/workflows/test.yaml/badge.svg)
6
+ ![release workflow](https://github.com/itsluketwist/llm-codegen-research/actions/workflows/release.yaml/badge.svg)
7
+
8
+
9
+ <div>
10
+ <!-- badges from : https://shields.io/ -->
11
+ <!-- logos available : https://simpleicons.org/ -->
12
+ <a href="https://opensource.org/licenses/MIT">
13
+ <img alt="MIT License" src="https://img.shields.io/badge/Licence-MIT-yellow?style=for-the-badge&logo=docs&logoColor=white" />
14
+ </a>
15
+ <a href="https://www.python.org/">
16
+ <img alt="Python 3" src="https://img.shields.io/badge/Python_3-blue?style=for-the-badge&logo=python&logoColor=white" />
17
+ </a>
18
+ <a href="https://openai.com/blog/openai-api/">
19
+ <img alt="OpenAI API" src="https://img.shields.io/badge/OpenAI_API-412991?style=for-the-badge&logo=openai&logoColor=white" />
20
+ </a>
21
+ <a href="https://www.anthropic.com/api/">
22
+ <img alt="Anthropic API" src="https://img.shields.io/badge/Claude_API-D97757?style=for-the-badge&logo=claude&logoColor=white" />
23
+ </a>
24
+ <a href="https://api.together.ai/">
25
+ <img alt="together.ai API" src="https://img.shields.io/badge/together.ai_API-B5B5B5?style=for-the-badge&logoColor=white" />
26
+ </a>
27
+ </div>
28
+
29
+
30
+ ## *usage*
31
+
32
+ A collection of methods and classes I repeatedly use when conducting research on LLM code-generation.
33
+ Covers both prompting various LLMs, and analysing the markdown responses.
34
+
35
+ ```python
36
+ from llm_cgr import quick_complete, Markdown
37
+
38
+ response = quick_complete("Write python code to generate the nth fibonacci number.")
39
+
40
+ markdown = Markdown(text=response)
41
+ ```
42
+
43
+ ## *installation*
44
+
45
+ Install directly from PyPI, using pip:
46
+
47
+ ```shell
48
+ pip install llm-codegen-research
49
+ ```
50
+
51
+ ## *development*
52
+
53
+ Clone the repository code:
54
+
55
+ ```shell
56
+ git clone https://github.com/itsluketwist/llm-codegen-research.git
57
+ ```
58
+
59
+ We use [`uv`](https://astral.sh/blog/uv) for project management.
60
+ Once cloned, create a virtual environment and install uv and the project:
61
+
62
+ ```shell
63
+ python -m venv .venv
64
+
65
+ . .venv/bin/activate
66
+
67
+ pip install uv
68
+
69
+ uv sync
70
+ ```
71
+
72
+ Use `make` commands to lint and test:
73
+
74
+ ```shell
75
+ make lint
76
+
77
+ make test
78
+ ```
79
+
80
+ Use `uv` to add new dependencies into the project and `uv.lock`:
81
+
82
+ ```shell
83
+ uv add openai
84
+ ```
@@ -0,0 +1,39 @@
1
+ # Package specification, as defined here:
2
+ # https://packaging.python.org/en/latest/specifications/pyproject-toml/#pyproject-toml-spec
3
+
4
+ [build-system]
5
+ requires = ["setuptools", "setuptools-git-versioning<2"]
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [tool.setuptools-git-versioning]
9
+ enabled = true
10
+
11
+ [project]
12
+ name = "llm-codegen-research"
13
+ dynamic = ["version"]
14
+ description = "Utilities for use when research code-generation by LLMs."
15
+ readme = "README.md"
16
+ authors = [{ name = "Lukas Twist", email = "itsluketwist@gmail.com" }]
17
+ license-files = ["LICENSE"]
18
+ classifiers = [
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3",
21
+ ]
22
+ keywords = ["llm", "code-generation", "research", "prompting", "nlp"]
23
+ requires-python = ">=3.11"
24
+ dependencies = [
25
+ "anthropic>=0.49.0",
26
+ "openai>=1.78.1",
27
+ "together>=1.5.8",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/itsluketwist/llm-codegen-research"
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "pre-commit>=4.2.0",
36
+ "pytest>=8.3.5",
37
+ "pytest-cov>=6.1.1",
38
+ "uv>=0.7.4",
39
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from llm_cgr import analyse, query
2
+ from llm_cgr.analyse import CodeBlock, CodeData, Markdown, analyse_code
3
+ from llm_cgr.json_utils import read_json, save_json
4
+ from llm_cgr.query import get_client, query_list, quick_generate
5
+
6
+
7
+ __all__ = [
8
+ "analyse",
9
+ "query",
10
+ "CodeBlock",
11
+ "CodeData",
12
+ "Markdown",
13
+ "analyse_code",
14
+ "read_json",
15
+ "save_json",
16
+ "get_client",
17
+ "query_list",
18
+ "quick_generate",
19
+ ]
@@ -0,0 +1,10 @@
1
+ from llm_cgr.analyse.classes import CodeBlock, Markdown
2
+ from llm_cgr.analyse.languages import CodeData, analyse_code
3
+
4
+
5
+ __all__ = [
6
+ "CodeBlock",
7
+ "Markdown",
8
+ "CodeData",
9
+ "analyse_code",
10
+ ]
@@ -0,0 +1,139 @@
1
+ """Classes for handling markdown responses from LLMs."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from llm_cgr.analyse.languages import analyse_code
6
+ from llm_cgr.analyse.regexes import CODE_BLOCK_REGEX
7
+ from llm_cgr.defaults import DEFAULT_CODEBLOCK_LANGUAGE
8
+
9
+
10
+ @dataclass
11
+ class CodeBlock:
12
+ """
13
+ A class to represent a block of code with it's language.
14
+ """
15
+
16
+ language: str | None
17
+ text: str
18
+ valid: bool | None # None if validity unknown, language not supported
19
+ error: str | None # only set if not valid
20
+ defined_funcs: list[str]
21
+ called_funcs: list[str]
22
+ packages: list[str]
23
+ imports: list[str]
24
+
25
+ def __init__(
26
+ self,
27
+ language: str | None,
28
+ text: str,
29
+ default_language: str | None = DEFAULT_CODEBLOCK_LANGUAGE,
30
+ ):
31
+ self.language = language.strip().lower() if language else None
32
+ self.text = text.strip()
33
+
34
+ code_data = analyse_code(
35
+ code=self.text, language=self.language or default_language
36
+ )
37
+
38
+ if self.language is None and not code_data.valid:
39
+ # if we hit errors after defaulting the language, ignore the results!
40
+ self.valid = None
41
+ self.error = None
42
+ self.defined_funcs = []
43
+ self.called_funcs = []
44
+ self.packages = []
45
+ self.imports = []
46
+
47
+ else:
48
+ self.language = self.language or default_language
49
+ self.valid = code_data.valid
50
+ self.error = code_data.error
51
+ self.defined_funcs = code_data.defined_funcs
52
+ self.called_funcs = code_data.called_funcs
53
+ self.packages = code_data.packages
54
+ self.imports = code_data.imports
55
+
56
+ def __repr__(self):
57
+ _language = f"language={self.language or 'unspecified'}"
58
+ _lines = f"lines={len(self.text.splitlines())}"
59
+ return f"{self.__class__.__name__}({_language}, {_lines})"
60
+
61
+ def __str__(self):
62
+ return self.text
63
+
64
+ @property
65
+ def markdown(self):
66
+ return f"```{self.language or ''}\n{self.text}\n```"
67
+
68
+
69
+ @dataclass
70
+ class Markdown:
71
+ """
72
+ A class to hold a markdown response from an LLM as a series of text and code blocks.
73
+ """
74
+
75
+ text: str
76
+ code_blocks: list[CodeBlock]
77
+ code_errors: list[str]
78
+ languages: list[str]
79
+
80
+ def __init__(
81
+ self,
82
+ text: str,
83
+ default_codeblock_language: str | None = DEFAULT_CODEBLOCK_LANGUAGE,
84
+ ):
85
+ """
86
+ Initialise the MarkdownResponse object with the text and code blocks.
87
+ Use `codeblock_language` when no language is specified for a code block.
88
+ """
89
+ self.text = text
90
+ self.code_blocks = self.extract_code_blocks(
91
+ response=text,
92
+ default_language=default_codeblock_language,
93
+ )
94
+ self.code_errors = [
95
+ f"{i}: {cb.error}" for i, cb in enumerate(self.code_blocks) if cb.error
96
+ ]
97
+ self.languages = sorted(
98
+ list({bl.language for bl in self.code_blocks if bl.language})
99
+ )
100
+
101
+ def __repr__(self):
102
+ _lines = f"lines={len(self.text.splitlines())}"
103
+ _code_blocks = f"code_blocks={len(self.code_blocks)}"
104
+ _languages = f"languages={','.join(self.languages)}"
105
+ return f"{self.__class__.__name__}({_lines}, {_code_blocks}, {_languages})"
106
+
107
+ def __str__(self):
108
+ return self.text
109
+
110
+ @staticmethod
111
+ def extract_code_blocks(
112
+ response: str,
113
+ default_language: str | None = DEFAULT_CODEBLOCK_LANGUAGE,
114
+ ) -> list[CodeBlock]:
115
+ """
116
+ Extract the code blocks from the markdown formatted text.
117
+ """
118
+ matches = CODE_BLOCK_REGEX.findall(string=response)
119
+ blocks = []
120
+ for match in matches:
121
+ language, text = match
122
+ blocks.append(
123
+ CodeBlock(
124
+ language=language if language else None,
125
+ text=text,
126
+ default_language=default_language,
127
+ )
128
+ )
129
+ return blocks
130
+
131
+ def first_code_block(self, language: str) -> CodeBlock | None:
132
+ """
133
+ Return the first code block of a certain language in the response.
134
+ """
135
+ for block in self.code_blocks:
136
+ if block.language == language:
137
+ return block
138
+
139
+ return None
@@ -0,0 +1,24 @@
1
+ from llm_cgr.analyse.languages.code_data import CodeData
2
+ from llm_cgr.analyse.languages.python import analyse_python_code
3
+
4
+
5
+ def analyse_code(code: str, language: str | None) -> CodeData:
6
+ """
7
+ Analyse code based on the language.
8
+ """
9
+ try:
10
+ if language == "python":
11
+ return analyse_python_code(code=code)
12
+ except Exception as exc:
13
+ return CodeData(
14
+ valid=False,
15
+ error=str(exc),
16
+ )
17
+
18
+ return CodeData()
19
+
20
+
21
+ __all__ = [
22
+ "analyse_code",
23
+ "CodeData",
24
+ ]
@@ -0,0 +1,34 @@
1
+ """Define the CodeData class, to store code analysis data."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Iterable
5
+
6
+
7
+ @dataclass
8
+ class CodeData:
9
+ """
10
+ A class to hold code analysis data.
11
+ """
12
+
13
+ valid: bool | None
14
+ error: str | None
15
+ defined_funcs: list[str]
16
+ called_funcs: list[str]
17
+ packages: list[str]
18
+ imports: list[str]
19
+
20
+ def __init__(
21
+ self,
22
+ valid: bool | None = None,
23
+ error: str | None = None,
24
+ defined_funcs: Iterable | None = None,
25
+ called_funcs: Iterable | None = None,
26
+ packages: Iterable | None = None,
27
+ imports: Iterable | None = None,
28
+ ):
29
+ self.valid = valid
30
+ self.error = error
31
+ self.defined_funcs = sorted(defined_funcs) if defined_funcs else []
32
+ self.called_funcs = sorted(called_funcs) if called_funcs else []
33
+ self.packages = sorted(packages) if packages else []
34
+ self.imports = sorted(imports) if imports else []
@@ -0,0 +1,86 @@
1
+ """Utility functions for Python code analysis."""
2
+
3
+ import ast
4
+ import sys
5
+
6
+ from llm_cgr.analyse.languages.code_data import CodeData
7
+
8
+
9
+ PYTHON_STDLIB = getattr(
10
+ sys, "stdlib_module_names", []
11
+ ) # use this below to determine packages
12
+
13
+
14
+ class PythonAnalyser(ast.NodeVisitor):
15
+ def __init__(self):
16
+ self.defined_funcs: set[str] = set()
17
+ self.called_funcs: set[str] = set()
18
+ self.imports: set[str] = set()
19
+ self.packages: set[str] = set()
20
+
21
+ def visit_FunctionDef(self, node: ast.FunctionDef):
22
+ # save defined functions
23
+ self.defined_funcs.add(node.name)
24
+ self.generic_visit(node)
25
+
26
+ def visit_Call(self, node: ast.Call):
27
+ func = node.func
28
+
29
+ # save `foo()` function calls
30
+ if isinstance(func, ast.Name):
31
+ self.called_funcs.add(func.id)
32
+
33
+ # save `lib.method()` function calls
34
+ elif isinstance(func, ast.Attribute):
35
+ if isinstance(func.value, ast.Name):
36
+ self.called_funcs.add(f"{func.value.id}.{func.attr}")
37
+ else:
38
+ self.called_funcs.add(func.attr)
39
+
40
+ self.generic_visit(node)
41
+
42
+ def visit_Import(self, node: ast.Import):
43
+ # save `import module` imports
44
+ for alias in node.names:
45
+ # save all imports
46
+ self.imports.add(alias.name)
47
+
48
+ # save external packages
49
+ top_level = alias.name.split(".")[0]
50
+ if top_level not in PYTHON_STDLIB:
51
+ self.packages.add(top_level)
52
+
53
+ self.generic_visit(node)
54
+
55
+ def visit_ImportFrom(self, node: ast.ImportFrom):
56
+ # save `from module import thing` imports
57
+ module = node.module or ""
58
+
59
+ # save external packages
60
+ # node.level is 0 for absolute imports, 1+ for relative imports
61
+ if module and module not in PYTHON_STDLIB and node.level == 0:
62
+ self.packages.add(module.split(".")[0])
63
+
64
+ # save all imports
65
+ for alias in node.names:
66
+ full = f"{module}.{alias.name}" if module else alias.name
67
+ self.imports.add(full)
68
+
69
+ self.generic_visit(node)
70
+
71
+
72
+ def analyse_python_code(code: str) -> CodeData:
73
+ """
74
+ Analyse Python code to extract functions and imports.
75
+ """
76
+ tree = ast.parse(code)
77
+ analyser = PythonAnalyser()
78
+ analyser.visit(tree)
79
+ return CodeData(
80
+ valid=True,
81
+ defined_funcs=analyser.defined_funcs,
82
+ called_funcs=analyser.called_funcs,
83
+ # packages lowercase for analysis (names are case-insensitive outside of code)
84
+ packages=[p.lower() for p in analyser.packages],
85
+ imports=analyser.imports,
86
+ )
@@ -0,0 +1,10 @@
1
+ """Regexes used to parse markdown."""
2
+
3
+ import re
4
+
5
+
6
+ # regex to extract code blocks from markdown text
7
+ CODE_BLOCK_REGEX = re.compile(
8
+ pattern=r"\`\`\`(\w*)\n(.*?)(?:\`\`\`|$)",
9
+ flags=re.DOTALL,
10
+ )
@@ -0,0 +1,10 @@
1
+ """Collection of default values for the llm_cgr package."""
2
+
3
+ # the default model to be used across tasks
4
+ DEFAULT_MODEL = "gpt-4.1-mini-2025-04-14"
5
+
6
+ # the default language to assume for code blocks if not specified
7
+ DEFAULT_CODEBLOCK_LANGUAGE = "python"
8
+
9
+ # the default max_tokens to be used when prompting models
10
+ DEFAULT_MAX_TOKENS = 2000
@@ -0,0 +1,24 @@
1
+ """Simple utility functions to save and read json files."""
2
+
3
+ import json
4
+
5
+
6
+ def save_json(
7
+ data: dict | list,
8
+ file_path: str,
9
+ ):
10
+ """
11
+ Utility to save python dictionary or list to a json file.
12
+ """
13
+ with open(file_path, mode="w", encoding="utf-8") as f:
14
+ json.dump(obj=data, fp=f, indent=4)
15
+
16
+
17
+ def read_json(
18
+ file_path: str,
19
+ ) -> dict | list:
20
+ """
21
+ Utility to load python dictionary or list from a json file.
22
+ """
23
+ with open(file_path, mode="r", encoding="utf-8") as f:
24
+ return json.load(fp=f)
File without changes
@@ -0,0 +1,18 @@
1
+ from llm_cgr.query.api_utils import get_client, query_list, quick_generate
2
+ from llm_cgr.query.generate import (
3
+ AnthropicGenerationAPI,
4
+ OpenAIGenerationAPI,
5
+ TogetherGenerationAPI,
6
+ )
7
+ from llm_cgr.query.protocol import GenerationProtocol
8
+
9
+
10
+ __all__ = [
11
+ "get_client",
12
+ "query_list",
13
+ "quick_generate",
14
+ "AnthropicGenerationAPI",
15
+ "OpenAIGenerationAPI",
16
+ "TogetherGenerationAPI",
17
+ "GenerationProtocol",
18
+ ]
@@ -0,0 +1,99 @@
1
+ """API utilities for interfacing with the generation models."""
2
+
3
+ from typing import Literal
4
+
5
+ from llm_cgr.defaults import DEFAULT_MODEL
6
+ from llm_cgr.query.generate import (
7
+ AnthropicGenerationAPI,
8
+ OpenAIGenerationAPI,
9
+ TogetherGenerationAPI,
10
+ )
11
+ from llm_cgr.query.prompts import (
12
+ BASE_SYSTEM_PROMPT,
13
+ CODE_SYSTEM_PROMPT,
14
+ LIST_SYSTEM_PROMPT,
15
+ )
16
+ from llm_cgr.query.protocol import GenerationProtocol
17
+
18
+
19
+ def get_client(
20
+ model: str,
21
+ type: Literal["base", "code", "list"] | None = None,
22
+ ) -> GenerationProtocol:
23
+ """
24
+ Initialise the correct generation interface for the given model.
25
+ """
26
+ _system = None
27
+ match type:
28
+ case "base":
29
+ _system = BASE_SYSTEM_PROMPT
30
+ case "code":
31
+ _system = CODE_SYSTEM_PROMPT
32
+ case "list":
33
+ _system = LIST_SYSTEM_PROMPT
34
+
35
+ if "claude" in model:
36
+ return AnthropicGenerationAPI(model=model, system=_system)
37
+
38
+ if "gpt" in model or "o1" in model:
39
+ return OpenAIGenerationAPI(model=model, system=_system)
40
+
41
+ return TogetherGenerationAPI(model=model, system=_system)
42
+
43
+
44
+ def quick_generate(
45
+ user: str,
46
+ type: Literal["base", "code", "list"] | None = None,
47
+ model: str = DEFAULT_MODEL,
48
+ system: str | None = None,
49
+ temperature: float | None = None,
50
+ ) -> str:
51
+ """
52
+ Simple function to quickly prompt a model for a response.
53
+ """
54
+ client = get_client(model=model, type=type)
55
+
56
+ [result] = client.generate(
57
+ user=user,
58
+ system=system,
59
+ temperature=temperature,
60
+ )
61
+ return result
62
+
63
+
64
+ def query_list(
65
+ user: str,
66
+ system: str = LIST_SYSTEM_PROMPT,
67
+ model: str = DEFAULT_MODEL,
68
+ ) -> list[str]:
69
+ """
70
+ Simple function to quickly prompt a model for a list of words.
71
+ """
72
+ _response = quick_generate(
73
+ user=user,
74
+ system=system,
75
+ model=model,
76
+ )
77
+
78
+ # sometimes the LLM will return a code block with the list inside it
79
+ if _response.startswith("```python"):
80
+ _response = _response.split("```python")[1]
81
+ if _response.endswith("```"):
82
+ _response = _response.split("```")[0]
83
+ _response = _response.strip()
84
+
85
+ try:
86
+ _list = eval(_response)
87
+ except Exception as e:
88
+ print(f"Error evaluating response.\nresponse: {_response}\nexception: {e}")
89
+ _list = []
90
+
91
+ if not isinstance(_list, list):
92
+ print(f"Error querying list. Response is not a list: {_list}")
93
+ _list = []
94
+
95
+ if any(not isinstance(item, str) for item in _list):
96
+ print(f"Error querying list. Response contains non-string items: {_list}")
97
+ _list = []
98
+
99
+ return _list
@@ -0,0 +1,149 @@
1
+ """Classes for access to generation APIs of LLM services."""
2
+
3
+ import anthropic
4
+ import openai
5
+ import together
6
+
7
+ from llm_cgr.defaults import DEFAULT_MAX_TOKENS
8
+
9
+
10
+ class OpenAIGenerationAPI:
11
+ """
12
+ Class to access the OpenAI API.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ model: str | None = None,
18
+ system: str | None = None,
19
+ ) -> None:
20
+ self._model = model
21
+ self._system = system or openai.NOT_GIVEN
22
+ self._client = openai.OpenAI()
23
+
24
+ def generate(
25
+ self,
26
+ user: str,
27
+ system: str | None = None,
28
+ model: str | None = None,
29
+ samples: int = 1,
30
+ temperature: float | None = None,
31
+ max_tokens: int | None = None,
32
+ ) -> list[str]:
33
+ """
34
+ Generate a model response from the OpenAI API.
35
+
36
+ Returns
37
+ -------
38
+ The text response to the prompt.
39
+ """
40
+ _generations = []
41
+ for _ in range(samples):
42
+ response = self._client.responses.create(
43
+ input=user,
44
+ model=model or self._model,
45
+ instructions=system or self._system,
46
+ temperature=temperature or openai.NOT_GIVEN,
47
+ max_output_tokens=max_tokens or openai.NOT_GIVEN,
48
+ )
49
+ _generations.append(response.output_text)
50
+
51
+ return _generations
52
+
53
+
54
+ class TogetherGenerationAPI:
55
+ """
56
+ Class to access the TogetherAI API.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ model: str | None = None,
62
+ system: str | None = None,
63
+ ) -> None:
64
+ self._model = model
65
+ self._system = system
66
+ self._client = together.Together()
67
+
68
+ def generate(
69
+ self,
70
+ user: str,
71
+ system: str | None = None,
72
+ model: str | None = None,
73
+ samples: int = 1,
74
+ temperature: float | None = None,
75
+ max_tokens: int | None = None,
76
+ ) -> list[str]:
77
+ """
78
+ Generate a model response from the TogetherAI API.
79
+
80
+ Returns
81
+ -------
82
+ The text response to the prompt.
83
+ """
84
+ _input = [{"role": "user", "content": user}]
85
+ _system = system or self._system
86
+ if _system:
87
+ _input = [{"role": "system", "content": _system}] + _input
88
+
89
+ _generations = []
90
+ for _ in range(samples):
91
+ response = self._client.chat.completions.create(
92
+ model=model or self._model,
93
+ messages=_input,
94
+ temperature=temperature,
95
+ max_tokens=max_tokens,
96
+ )
97
+ _generations.append(response.choices[0].message.content)
98
+
99
+ return _generations
100
+
101
+
102
+ class AnthropicGenerationAPI:
103
+ """
104
+ Class to access the Anthropic Claude API.
105
+ """
106
+
107
+ def __init__(self, model: str | None = None, system: str | None = None) -> None:
108
+ self._model = model
109
+ self._system = system or anthropic.NOT_GIVEN
110
+ self._client = anthropic.Anthropic()
111
+
112
+ def generate(
113
+ self,
114
+ user: str,
115
+ system: str | None = None,
116
+ model: str | None = None,
117
+ samples: int = 1,
118
+ temperature: float | None = None,
119
+ max_tokens: int | None = None,
120
+ ) -> list[str]:
121
+ """
122
+ Generate a model response from the Anthropic Claude API.
123
+
124
+ Returns
125
+ -------
126
+ The text response to the prompt.
127
+ """
128
+ _generations = []
129
+ for _ in range(samples):
130
+ response = self._client.messages.create(
131
+ model=model or self._model,
132
+ system=system or self._system,
133
+ messages=[
134
+ {
135
+ "role": "user",
136
+ "content": [
137
+ {
138
+ "type": "text",
139
+ "text": user,
140
+ },
141
+ ],
142
+ },
143
+ ],
144
+ temperature=temperature or anthropic.NOT_GIVEN,
145
+ max_tokens=max_tokens or DEFAULT_MAX_TOKENS,
146
+ )
147
+ _generations.append(response.content[0].text)
148
+
149
+ return _generations
@@ -0,0 +1,13 @@
1
+ """All prompts used in the project are defined here."""
2
+
3
+ # the default system prompt to be used across tasks
4
+ BASE_SYSTEM_PROMPT = "You are a helpful assistant."
5
+
6
+ # the default system prompt to be used across coding tasks
7
+ CODE_SYSTEM_PROMPT = "You are a helpful and knowledgeable code assistant!"
8
+
9
+ # the system prompt to be used for generating lists of words
10
+ LIST_SYSTEM_PROMPT = (
11
+ "You are a helpful assistant that provides lists of words.\n"
12
+ "You only respond in correctly formatted python lists, containing only strings."
13
+ )
@@ -0,0 +1,20 @@
1
+ """Protocol for the completion API of an LLM service."""
2
+
3
+ from typing import Protocol
4
+
5
+
6
+ class GenerationProtocol(Protocol):
7
+ """
8
+ Protocol that describes how to access the generation API of an LLM service.
9
+ """
10
+
11
+ def generate(
12
+ self,
13
+ user: str,
14
+ system: str | None = None,
15
+ model: str | None = None,
16
+ samples: int = 1,
17
+ temperature: float | None = None,
18
+ max_tokens: int | None = None,
19
+ ) -> list[str]:
20
+ pass
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm-codegen-research
3
+ Version: 1.2
4
+ Summary: Utilities for use when research code-generation by LLMs.
5
+ Author-email: Lukas Twist <itsluketwist@gmail.com>
6
+ Project-URL: Homepage, https://github.com/itsluketwist/llm-codegen-research
7
+ Keywords: llm,code-generation,research,prompting,nlp
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: anthropic>=0.49.0
14
+ Requires-Dist: openai>=1.78.1
15
+ Requires-Dist: together>=1.5.8
16
+ Dynamic: license-file
17
+
18
+ # **llm-codegen-research**
19
+
20
+
21
+ ![lint code workflow](https://github.com/itsluketwist/llm-codegen-research/actions/workflows/lint.yaml/badge.svg)
22
+ ![test code workflow](https://github.com/itsluketwist/llm-codegen-research/actions/workflows/test.yaml/badge.svg)
23
+ ![release workflow](https://github.com/itsluketwist/llm-codegen-research/actions/workflows/release.yaml/badge.svg)
24
+
25
+
26
+ <div>
27
+ <!-- badges from : https://shields.io/ -->
28
+ <!-- logos available : https://simpleicons.org/ -->
29
+ <a href="https://opensource.org/licenses/MIT">
30
+ <img alt="MIT License" src="https://img.shields.io/badge/Licence-MIT-yellow?style=for-the-badge&logo=docs&logoColor=white" />
31
+ </a>
32
+ <a href="https://www.python.org/">
33
+ <img alt="Python 3" src="https://img.shields.io/badge/Python_3-blue?style=for-the-badge&logo=python&logoColor=white" />
34
+ </a>
35
+ <a href="https://openai.com/blog/openai-api/">
36
+ <img alt="OpenAI API" src="https://img.shields.io/badge/OpenAI_API-412991?style=for-the-badge&logo=openai&logoColor=white" />
37
+ </a>
38
+ <a href="https://www.anthropic.com/api/">
39
+ <img alt="Anthropic API" src="https://img.shields.io/badge/Claude_API-D97757?style=for-the-badge&logo=claude&logoColor=white" />
40
+ </a>
41
+ <a href="https://api.together.ai/">
42
+ <img alt="together.ai API" src="https://img.shields.io/badge/together.ai_API-B5B5B5?style=for-the-badge&logoColor=white" />
43
+ </a>
44
+ </div>
45
+
46
+
47
+ ## *usage*
48
+
49
+ A collection of methods and classes I repeatedly use when conducting research on LLM code-generation.
50
+ Covers both prompting various LLMs, and analysing the markdown responses.
51
+
52
+ ```python
53
+ from llm_cgr import quick_complete, Markdown
54
+
55
+ response = quick_complete("Write python code to generate the nth fibonacci number.")
56
+
57
+ markdown = Markdown(text=response)
58
+ ```
59
+
60
+ ## *installation*
61
+
62
+ Install directly from PyPI, using pip:
63
+
64
+ ```shell
65
+ pip install llm-codegen-research
66
+ ```
67
+
68
+ ## *development*
69
+
70
+ Clone the repository code:
71
+
72
+ ```shell
73
+ git clone https://github.com/itsluketwist/llm-codegen-research.git
74
+ ```
75
+
76
+ We use [`uv`](https://astral.sh/blog/uv) for project management.
77
+ Once cloned, create a virtual environment and install uv and the project:
78
+
79
+ ```shell
80
+ python -m venv .venv
81
+
82
+ . .venv/bin/activate
83
+
84
+ pip install uv
85
+
86
+ uv sync
87
+ ```
88
+
89
+ Use `make` commands to lint and test:
90
+
91
+ ```shell
92
+ make lint
93
+
94
+ make test
95
+ ```
96
+
97
+ Use `uv` to add new dependencies into the project and `uv.lock`:
98
+
99
+ ```shell
100
+ uv add openai
101
+ ```
@@ -0,0 +1,25 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/llm_cgr/__init__.py
5
+ src/llm_cgr/defaults.py
6
+ src/llm_cgr/json_utils.py
7
+ src/llm_cgr/py.typed
8
+ src/llm_cgr/analyse/__init__.py
9
+ src/llm_cgr/analyse/classes.py
10
+ src/llm_cgr/analyse/regexes.py
11
+ src/llm_cgr/analyse/languages/__init__.py
12
+ src/llm_cgr/analyse/languages/code_data.py
13
+ src/llm_cgr/analyse/languages/python.py
14
+ src/llm_cgr/query/__init__.py
15
+ src/llm_cgr/query/api_utils.py
16
+ src/llm_cgr/query/generate.py
17
+ src/llm_cgr/query/prompts.py
18
+ src/llm_cgr/query/protocol.py
19
+ src/llm_codegen_research.egg-info/PKG-INFO
20
+ src/llm_codegen_research.egg-info/SOURCES.txt
21
+ src/llm_codegen_research.egg-info/dependency_links.txt
22
+ src/llm_codegen_research.egg-info/requires.txt
23
+ src/llm_codegen_research.egg-info/top_level.txt
24
+ tests/test_analyse.py
25
+ tests/test_query.py
@@ -0,0 +1,3 @@
1
+ anthropic>=0.49.0
2
+ openai>=1.78.1
3
+ together>=1.5.8
@@ -0,0 +1,125 @@
1
+ from llm_cgr import Markdown
2
+
3
+
4
+ TEST_LLM_RESPONSE = """
5
+ Here's a Python solution to process some data and return an answer.
6
+
7
+ ```python
8
+ import numpy as np
9
+ from requests import get
10
+ import json
11
+ from collections import defaultdict
12
+ from cryptography.fernet import Fernet
13
+ import pandas.DataFrame
14
+
15
+ def process_data(data):
16
+ response = get("https://api.example.com/data")
17
+ data = np.array([1, 2, 3, 4, 5])
18
+ return np.process(data, response)
19
+ ```
20
+
21
+ Some more code:
22
+
23
+ ```
24
+ import pandas as pd
25
+
26
+ csv = pd.read_csv("data.csv")
27
+ ```
28
+
29
+ Run some code:
30
+
31
+ ```bash
32
+ python script.py
33
+ ```
34
+
35
+ Some very bad python code:
36
+
37
+ ```python
38
+ import problem
39
+
40
+ problem.bad_brackets((()
41
+ ```
42
+
43
+ Some very bad unknown code:
44
+
45
+ ```
46
+ for import xxx)[
47
+ ```
48
+ """
49
+
50
+
51
+ def test_markdown():
52
+ """
53
+ Test the MarkdownResponse class, extracting and analysing multiple code blocks.
54
+ """
55
+ # parse the response
56
+ analysed = Markdown(text=TEST_LLM_RESPONSE)
57
+
58
+ # check initial properties
59
+ assert analysed.text == TEST_LLM_RESPONSE
60
+ assert len(analysed.code_blocks) == 5
61
+ assert analysed.code_errors == ["3: '(' was never closed (<unknown>, line 3)"]
62
+ assert analysed.languages == ["bash", "python"]
63
+
64
+ # expected python code block
65
+ python_code_one = analysed.code_blocks[0]
66
+ assert python_code_one.language == "python"
67
+ assert python_code_one.valid is True
68
+ assert python_code_one.error is None
69
+ assert python_code_one.defined_funcs == ["process_data"]
70
+ assert python_code_one.called_funcs == ["get", "np.array", "np.process"]
71
+ assert python_code_one.packages == [
72
+ "cryptography",
73
+ "numpy",
74
+ "pandas",
75
+ "requests",
76
+ ]
77
+ assert python_code_one.imports == [
78
+ "collections.defaultdict",
79
+ "cryptography.fernet.Fernet",
80
+ "json",
81
+ "numpy",
82
+ "pandas.DataFrame",
83
+ "requests.get",
84
+ ]
85
+
86
+ # unspecified code block defaults to python
87
+ python_code_two = analysed.code_blocks[1]
88
+ assert python_code_two.language == "python"
89
+ assert python_code_two.valid is True
90
+ assert python_code_two.error is None
91
+ assert python_code_two.defined_funcs == []
92
+ assert python_code_two.called_funcs == ["pd.read_csv"]
93
+ assert python_code_two.packages == ["pandas"]
94
+ assert python_code_two.imports == ["pandas"]
95
+
96
+ # bash code block with no analysis
97
+ bash_code = analysed.code_blocks[2]
98
+ assert bash_code.language == "bash"
99
+ assert bash_code.valid is None
100
+ assert bash_code.error is None
101
+ assert bash_code.defined_funcs == []
102
+ assert bash_code.called_funcs == []
103
+ assert bash_code.packages == []
104
+ assert bash_code.imports == []
105
+
106
+ # python code block with incorrect syntax
107
+ bad_code = analysed.code_blocks[3]
108
+ assert bad_code.language == "python"
109
+ assert bad_code.valid is False
110
+ assert bad_code.error == "'(' was never closed (<unknown>, line 3)"
111
+ assert bad_code.defined_funcs == []
112
+ assert bad_code.called_funcs == []
113
+ assert bad_code.packages == []
114
+ assert bad_code.imports == []
115
+
116
+ # unknown code block
117
+ unknown_code = analysed.code_blocks[4]
118
+ assert unknown_code.text == "for import xxx)["
119
+ assert unknown_code.language is None
120
+ assert unknown_code.valid is None
121
+ assert unknown_code.error is None
122
+ assert unknown_code.defined_funcs == []
123
+ assert unknown_code.called_funcs == []
124
+ assert unknown_code.packages == []
125
+ assert unknown_code.imports == []
File without changes