vtk-prompt 0.0.3__py3-none-any.whl

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.
vtk_prompt/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """VTK-Prompt - CLI tool for generating VTK visualizations using LLMs.
2
+
3
+ This package provides tools to generate VTK Python code and XML files using
4
+ LLMs (Anthropic Claude, OpenAI GPT, or NVIDIA NIM models). It also includes
5
+ Retrieval-Augmented Generation (RAG) capabilities to improve code generation
6
+ by providing relevant examples from the VTK examples corpus.
7
+
8
+ Main components:
9
+ - vtk-prompt: Generate and run VTK Python code
10
+ - gen-vtk-file: Generate VTK XML files
11
+ - vtk-build-rag: Build a RAG database from VTK examples
12
+ - vtk-test-rag: Test the RAG database with queries
13
+ """
14
+
15
+ try:
16
+ from importlib.metadata import version, PackageNotFoundError
17
+ except ImportError:
18
+ from importlib_metadata import version, PackageNotFoundError
19
+
20
+ try:
21
+ __version__ = version("vtk-prompt")
22
+ except PackageNotFoundError:
23
+ __version__ = "unknown"
24
+ __author__ = "Vicente Adolfo Bolea Sanchez"
25
+ __email__ = "vicente.bolea@kitware.com"
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import importlib.util
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def setup_rag_path():
10
+ """Add rag-components to the Python path.
11
+
12
+ Returns:
13
+ The path to the rag-components directory
14
+ """
15
+ script_dir = Path(__file__).resolve().parent
16
+ project_root = script_dir.parent.parent
17
+ rag_path = str(project_root / "rag-components")
18
+
19
+ if rag_path not in sys.path:
20
+ sys.path.append(rag_path)
21
+
22
+ return rag_path
23
+
24
+
25
+ def check_dependencies():
26
+ """Check if required dependencies are installed.
27
+
28
+ Returns:
29
+ True if all dependencies are installed, False otherwise
30
+ """
31
+ required_modules = ["chromadb", "sentence_transformers", "tree_sitter_languages"]
32
+
33
+ for module in required_modules:
34
+ if importlib.util.find_spec(module) is None:
35
+ print(f"Missing required dependency: {module}")
36
+ return False
37
+
38
+ return True
39
+
40
+
41
+ def main():
42
+ """Build a RAG database from VTK example files."""
43
+ parser = argparse.ArgumentParser(description="Build RAG database for VTK examples")
44
+ parser.add_argument(
45
+ "--examples-dir",
46
+ type=str,
47
+ default="data/examples",
48
+ help="Directory containing VTK examples (default: data/examples)",
49
+ )
50
+ parser.add_argument(
51
+ "--database",
52
+ type=str,
53
+ default="./db/codesage-codesage-large-v2",
54
+ help="Database path (default: ./db/codesage-codesage-large-v2)",
55
+ )
56
+ parser.add_argument(
57
+ "--collection-name",
58
+ type=str,
59
+ default="vtk-examples",
60
+ help="Collection name in the database (default: vtk-examples)",
61
+ )
62
+ parser.add_argument(
63
+ "--model",
64
+ type=str,
65
+ default="codesage/codesage-large-v2",
66
+ help="Embedding model name (default: codesage/codesage-large-v2)",
67
+ )
68
+ parser.add_argument(
69
+ "--language",
70
+ type=str,
71
+ default="python",
72
+ help="Language of the examples (default: python)",
73
+ )
74
+
75
+ args = parser.parse_args()
76
+
77
+ # Check dependencies
78
+ if not check_dependencies():
79
+ print("Please install the required dependencies with:")
80
+ print('pip install -e ".[rag]"')
81
+ sys.exit(1)
82
+
83
+ # Setup RAG path
84
+ rag_path = setup_rag_path()
85
+
86
+ # Import populate_db from rag-components
87
+ try:
88
+ sys.path.insert(0, rag_path)
89
+ from populate_db import fill_database
90
+ except ImportError as e:
91
+ print(f"Failed to import from rag-components: {e}")
92
+ print(
93
+ "Make sure the rag-components directory exists and contains the required files."
94
+ )
95
+ sys.exit(1)
96
+
97
+ # Check if examples directory exists
98
+ examples_dir = Path(args.examples_dir)
99
+ if not examples_dir.exists() or not examples_dir.is_dir():
100
+ print(
101
+ f"Error: Examples directory '{args.examples_dir}' does not exist or is not a directory"
102
+ )
103
+ sys.exit(1)
104
+
105
+ # Get all Python files in the examples directory
106
+ files = list(examples_dir.glob("**/*.py"))
107
+ if not files:
108
+ print(f"No Python files found in '{args.examples_dir}'")
109
+ sys.exit(1)
110
+
111
+ print(f"Found {len(files)} Python files in '{args.examples_dir}'")
112
+
113
+ # Create database directory if it doesn't exist
114
+ database_dir = Path(args.database).parent
115
+ database_dir.mkdir(parents=True, exist_ok=True)
116
+
117
+ # Build the RAG database
118
+ print(
119
+ f"Building RAG database at '{args.database}' using embedding model '{args.model}'..."
120
+ )
121
+ try:
122
+ fill_database(
123
+ files=files,
124
+ database_path=args.database,
125
+ embedding_model=args.model,
126
+ language=args.language,
127
+ collection_name=args.collection_name,
128
+ )
129
+
130
+ print(f"Successfully built RAG database at '{args.database}'")
131
+ print("You can now use the RAG database with vtk-prompt by running:")
132
+ print(
133
+ f'vtk-prompt "your query" -r --database {args.database} '
134
+ f"--collection {args.collection_name}"
135
+ )
136
+
137
+ except Exception as e:
138
+ print(f"Error building RAG database: {e}")
139
+ import traceback
140
+
141
+ traceback.print_exc()
142
+ sys.exit(1)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from pathlib import Path
4
+ import os
5
+ import json
6
+ import sys
7
+ import openai
8
+ import click
9
+
10
+ # Import our template system
11
+ from .prompts import (
12
+ get_vtk_xml_context,
13
+ get_xml_role,
14
+ )
15
+
16
+
17
+ class VTKXMLGenerator:
18
+ """OpenAI client for VTK XML file generation."""
19
+
20
+ def __init__(self, api_key=None, base_url=None):
21
+ """Initialize the VTK XML generator.
22
+
23
+ Args:
24
+ api_key: OpenAI API key (defaults to OPENAI_API_KEY env var)
25
+ base_url: Optional custom API endpoint URL
26
+ """
27
+ self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
28
+ self.base_url = base_url
29
+
30
+ if not self.api_key:
31
+ raise ValueError(
32
+ "No API key provided. Set OPENAI_API_KEY or pass api_key parameter."
33
+ )
34
+
35
+ self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
36
+
37
+ def generate_xml(self, message, model, max_tokens=4000, temperature=0.7):
38
+ """Generate VTK XML content from a description."""
39
+ examples_path = Path("data/examples/index.json")
40
+ if examples_path.exists():
41
+ _ = " ".join(json.loads(examples_path.read_text()).keys())
42
+ else:
43
+ _ = ""
44
+
45
+ context = get_vtk_xml_context(message)
46
+
47
+ response = self.client.chat.completions.create(
48
+ model=model,
49
+ messages=[
50
+ {"role": "system", "content": get_xml_role()},
51
+ {"role": "user", "content": context},
52
+ ],
53
+ max_tokens=max_tokens,
54
+ temperature=temperature,
55
+ )
56
+
57
+ if hasattr(response, "choices") and len(response.choices) > 0:
58
+ content = response.choices[0].message.content or "No content in response"
59
+ finish_reason = response.choices[0].finish_reason
60
+
61
+ if finish_reason == "length":
62
+ raise ValueError(
63
+ f"Output was truncated due to max_tokens limit ({max_tokens}). "
64
+ "Please increase max_tokens."
65
+ )
66
+
67
+ return content
68
+
69
+ return "No response generated"
70
+
71
+
72
+ # Legacy function wrapper for backwards compatibility
73
+ def openai_query(message, model, api_key, max_tokens, temperature=0.7, base_url=None):
74
+ """Legacy wrapper for VTK XML generation."""
75
+ generator = VTKXMLGenerator(api_key, base_url)
76
+ return generator.generate_xml(message, model, max_tokens, temperature)
77
+
78
+
79
+ @click.command()
80
+ @click.argument("input_string")
81
+ @click.option(
82
+ "--provider",
83
+ type=click.Choice(["openai", "anthropic", "gemini", "nim"]),
84
+ default="openai",
85
+ help="LLM provider to use",
86
+ )
87
+ @click.option("-m", "--model", default="gpt-4o", help="Model to use for generation")
88
+ @click.option(
89
+ "-t", "--token", required=True, help="API token for the selected provider"
90
+ )
91
+ @click.option("--base-url", help="Base URL for API (auto-detected or custom)")
92
+ @click.option(
93
+ "-k",
94
+ "--max-tokens",
95
+ type=int,
96
+ default=4000,
97
+ help="Maximum number of tokens to generate",
98
+ )
99
+ @click.option(
100
+ "--temperature",
101
+ type=float,
102
+ default=0.7,
103
+ help="Temperature for generation (0.0-2.0)",
104
+ )
105
+ @click.option(
106
+ "-o", "--output", help="Output file path (if not specified, output to stdout)"
107
+ )
108
+ def main(
109
+ input_string, provider, model, token, base_url, max_tokens, temperature, output
110
+ ):
111
+ """Generate VTK XML file content using LLMs.
112
+
113
+ INPUT_STRING: Description of the VTK file to generate
114
+ """
115
+
116
+ # Set default base URLs
117
+ if not base_url:
118
+ base_urls = {
119
+ "anthropic": "https://api.anthropic.com/v1",
120
+ "gemini": "https://generativelanguage.googleapis.com/v1beta/openai/",
121
+ "nim": "https://integrate.api.nvidia.com/v1",
122
+ }
123
+ base_url = base_urls.get(provider)
124
+
125
+ # Set default models based on provider
126
+ if model == "gpt-4o":
127
+ default_models = {
128
+ "anthropic": "claude-3-5-sonnet-20241022",
129
+ "gemini": "gemini-1.5-pro",
130
+ "nim": "meta/llama3-70b-instruct",
131
+ }
132
+ model = default_models.get(provider, model)
133
+
134
+ # Initialize the VTK XML generator
135
+ try:
136
+ generator = VTKXMLGenerator(token, base_url)
137
+ except ValueError as e:
138
+ print(f"Error: {e}", file=sys.stderr)
139
+ sys.exit(1)
140
+
141
+ # Generate the VTK XML content
142
+ try:
143
+ xml_content = generator.generate_xml(
144
+ input_string, model, max_tokens, temperature
145
+ )
146
+ except ValueError as e:
147
+ if "max_tokens" in str(e):
148
+ print(f"\nError: {e}", file=sys.stderr)
149
+ print(f"Current max_tokens: {max_tokens}", file=sys.stderr)
150
+ print("Try increasing with: --max-tokens <higher_number>", file=sys.stderr)
151
+ else:
152
+ print(f"Error: {e}", file=sys.stderr)
153
+ sys.exit(1)
154
+
155
+ # Validate XML structure (basic check)
156
+ if xml_content.strip().startswith("<?xml") and "</VTKFile>" in xml_content:
157
+ # Output to file or stdout
158
+ if output:
159
+ with open(output, "w") as f:
160
+ f.write(xml_content)
161
+ print(f"VTK XML content written to {output}")
162
+ else:
163
+ print(xml_content)
164
+ else:
165
+ print("Warning: Generated content may not be valid VTK XML", file=sys.stderr)
166
+ if output:
167
+ with open(output, "w") as f:
168
+ f.write(xml_content)
169
+ print(f"Content written to {output} (please verify)")
170
+ else:
171
+ print(xml_content)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()
vtk_prompt/prompt.py ADDED
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import ast
4
+ import os
5
+ import sys
6
+ import openai
7
+ import click
8
+ from dataclasses import dataclass
9
+
10
+ from .prompts import (
11
+ get_no_rag_context,
12
+ get_rag_context,
13
+ get_python_role,
14
+ )
15
+
16
+
17
+ @dataclass
18
+ class VTKPromptClient:
19
+ """OpenAI client for VTK code generation."""
20
+
21
+ collection_name: str = "vtk-examples"
22
+ database_path: str = "./db/codesage-codesage-large-v2"
23
+ verbose: bool = False
24
+
25
+ def validate_code_syntax(self, code_string):
26
+ """Validate Python code syntax using AST."""
27
+ try:
28
+ ast.parse(code_string)
29
+ return True, None
30
+ except SyntaxError as e:
31
+ return False, f"Syntax error: {e.msg} at line {e.lineno}"
32
+ except Exception as e:
33
+ return False, f"AST parsing error: {str(e)}"
34
+
35
+ def run_code(self, code_string):
36
+ """Execute VTK code using exec() after AST validation."""
37
+ is_valid, error_msg = self.validate_code_syntax(code_string)
38
+ if not is_valid:
39
+ print(f"Code validation failed: {error_msg}")
40
+ if self.verbose:
41
+ print("Generated code:")
42
+ print(code_string)
43
+ return None
44
+
45
+ if self.verbose:
46
+ print(code_string)
47
+
48
+ try:
49
+ exec(code_string, globals(), {})
50
+ except Exception as e:
51
+ print(f"Error executing code: {e}")
52
+ if not self.verbose:
53
+ print(code_string)
54
+ return None
55
+
56
+ def query(
57
+ self,
58
+ message,
59
+ api_key,
60
+ model="gpt-4o",
61
+ base_url=None,
62
+ max_tokens=1000,
63
+ temperature=0.1,
64
+ top_k=5,
65
+ rag=False,
66
+ retry_attempts=1,
67
+ ):
68
+ """Generate VTK code with optional RAG enhancement and retry logic.
69
+
70
+ Args:
71
+ message: The user query
72
+ api_key: API key for the service
73
+ model: Model name to use
74
+ base_url: API base URL
75
+ max_tokens: Maximum tokens to generate
76
+ temperature: Temperature for generation
77
+ top_k: Number of RAG examples to retrieve
78
+ rag: Whether to use RAG enhancement
79
+ retry_attempts: Number of times to retry if AST validation fails
80
+ """
81
+ if not api_key:
82
+ api_key = os.environ.get("OPENAI_API_KEY")
83
+
84
+ if not api_key:
85
+ raise ValueError(
86
+ "No API key provided. Set OPENAI_API_KEY or pass api_key parameter."
87
+ )
88
+
89
+ # Create client with current parameters
90
+ client = openai.OpenAI(api_key=api_key, base_url=base_url)
91
+
92
+ if rag:
93
+ from .rag_chat_wrapper import (
94
+ check_rag_components_available,
95
+ get_rag_snippets,
96
+ )
97
+
98
+ if not check_rag_components_available():
99
+ raise ValueError("RAG components not available")
100
+
101
+ rag_snippets = get_rag_snippets(
102
+ message,
103
+ collection_name=self.collection_name,
104
+ database_path=self.database_path,
105
+ top_k=top_k,
106
+ )
107
+
108
+ if not rag_snippets:
109
+ raise ValueError("Failed to load RAG snippets")
110
+
111
+ context_snippets = "\n\n".join(rag_snippets["code_snippets"])
112
+ context = get_rag_context(message, context_snippets)
113
+
114
+ if self.verbose:
115
+ print("CONTEXT: " + context)
116
+ references = rag_snippets.get("references")
117
+ if references:
118
+ print("Using examples from:")
119
+ for ref in references:
120
+ print(f"- {ref}")
121
+ else:
122
+ context = get_no_rag_context(message)
123
+ if self.verbose:
124
+ print("CONTEXT: " + context)
125
+
126
+ messages = [
127
+ {"role": "system", "content": get_python_role()},
128
+ {"role": "user", "content": context},
129
+ ]
130
+
131
+ # Retry loop for AST validation
132
+ for attempt in range(retry_attempts):
133
+ if self.verbose:
134
+ if attempt > 0:
135
+ print(f"Retry attempt {attempt + 1}/{retry_attempts}")
136
+ print(f"Making request with model: {model}, temperature: {temperature}")
137
+ for i, msg in enumerate(messages):
138
+ print(f"Message {i} ({msg['role']}): {msg['content'][:100]}...")
139
+
140
+ response = client.chat.completions.create(
141
+ model=model,
142
+ messages=messages,
143
+ max_tokens=max_tokens,
144
+ temperature=temperature,
145
+ )
146
+
147
+ if hasattr(response, "choices") and len(response.choices) > 0:
148
+ content = (
149
+ response.choices[0].message.content or "No content in response"
150
+ )
151
+ finish_reason = response.choices[0].finish_reason
152
+
153
+ if finish_reason == "length":
154
+ raise ValueError(
155
+ f"Output was truncated due to max_tokens limit ({max_tokens}). Please increase max_tokens."
156
+ )
157
+
158
+ generated_code = None
159
+ if "import vtk" not in content:
160
+ generated_code = "import vtk\n" + content
161
+ else:
162
+ pos = content.find("import vtk")
163
+ if pos != -1:
164
+ generated_code = content[pos:]
165
+ else:
166
+ generated_code = content
167
+
168
+ is_valid, error_msg = self.validate_code_syntax(generated_code)
169
+ if is_valid:
170
+ return generated_code, response.usage
171
+
172
+ elif attempt < retry_attempts - 1: # Don't print on last attempt
173
+ if self.verbose:
174
+ print(f"AST validation failed: {error_msg}. Retrying...")
175
+ # Add error feedback to context for retry
176
+ messages.append({"role": "assistant", "content": content})
177
+ messages.append(
178
+ {
179
+ "role": "user",
180
+ "content": (
181
+ f"The generated code has a syntax error: {error_msg}. "
182
+ "Please fix the syntax and generate valid Python code."
183
+ ),
184
+ }
185
+ )
186
+ else:
187
+ # Last attempt failed
188
+ if self.verbose:
189
+ print(f"Final attempt failed AST validation: {error_msg}")
190
+ return (
191
+ generated_code,
192
+ response.usage,
193
+ ) # Return anyway, let caller handle
194
+ else:
195
+ if attempt == retry_attempts - 1:
196
+ return "No response generated", response.usage
197
+
198
+ return "No response generated"
199
+
200
+
201
+ @click.command()
202
+ @click.argument("input_string")
203
+ @click.option(
204
+ "--provider",
205
+ type=click.Choice(["openai", "anthropic", "gemini", "nim"]),
206
+ default="openai",
207
+ help="LLM provider to use",
208
+ )
209
+ @click.option("-m", "--model", default="gpt-4o", help="Model name to use")
210
+ @click.option(
211
+ "-k", "--max-tokens", type=int, default=1000, help="Max # of tokens to generate"
212
+ )
213
+ @click.option(
214
+ "--temperature",
215
+ type=float,
216
+ default=0.7,
217
+ help="Temperature for generation (0.0-2.0)",
218
+ )
219
+ @click.option(
220
+ "-t", "--token", required=True, help="API token for the selected provider"
221
+ )
222
+ @click.option("--base-url", help="Base URL for API (auto-detected or custom)")
223
+ @click.option("-r", "--rag", is_flag=True, help="Use RAG to improve code generation")
224
+ @click.option("-v", "--verbose", is_flag=True, help="Show generated source code")
225
+ @click.option("--collection", default="vtk-examples", help="Collection name for RAG")
226
+ @click.option(
227
+ "--database",
228
+ default="./db/codesage-codesage-large-v2",
229
+ help="Database path for RAG",
230
+ )
231
+ @click.option(
232
+ "--top-k", type=int, default=5, help="Number of examples to retrieve from RAG"
233
+ )
234
+ @click.option(
235
+ "--retry-attempts",
236
+ type=int,
237
+ default=1,
238
+ help="Number of times to retry if AST validation fails",
239
+ )
240
+ def main(
241
+ input_string,
242
+ provider,
243
+ model,
244
+ max_tokens,
245
+ temperature,
246
+ token,
247
+ base_url,
248
+ rag,
249
+ verbose,
250
+ collection,
251
+ database,
252
+ top_k,
253
+ retry_attempts,
254
+ ):
255
+ """Generate and execute VTK code using LLMs.
256
+
257
+ INPUT_STRING: The code description to generate VTK code for
258
+ """
259
+
260
+ # Set default base URLs
261
+ if base_url is None:
262
+ base_urls = {
263
+ "anthropic": "https://api.anthropic.com/v1",
264
+ "gemini": "https://generativelanguage.googleapis.com/v1beta/openai/",
265
+ "nim": "https://integrate.api.nvidia.com/v1",
266
+ }
267
+ base_url = base_urls.get(provider)
268
+
269
+ # Set default models based on provider
270
+ if model == "gpt-4o":
271
+ default_models = {
272
+ "anthropic": "claude-3-5-sonnet-20241022",
273
+ "gemini": "gemini-1.5-pro",
274
+ "nim": "meta/llama3-70b-instruct",
275
+ }
276
+ model = default_models.get(provider, model)
277
+
278
+ try:
279
+ client = VTKPromptClient(
280
+ collection_name=collection,
281
+ database_path=database,
282
+ verbose=verbose,
283
+ )
284
+ generated_code, usage = client.query(
285
+ input_string,
286
+ api_key=token,
287
+ model=model,
288
+ base_url=base_url,
289
+ max_tokens=max_tokens,
290
+ temperature=temperature,
291
+ top_k=top_k,
292
+ rag=rag,
293
+ retry_attempts=retry_attempts,
294
+ )
295
+
296
+ if verbose and usage is not None:
297
+ print(
298
+ f"Used tokens: input={usage.prompt_tokens} output={usage.completion_tokens}"
299
+ )
300
+
301
+ client.run_code(generated_code)
302
+
303
+ except ValueError as e:
304
+ if "RAG components" in str(e):
305
+ print("rag_components not found", file=sys.stderr)
306
+ sys.exit(1)
307
+ elif "Failed to load RAG snippets" in str(e):
308
+ print("failed to load rag snippets", file=sys.stderr)
309
+ sys.exit(2)
310
+
311
+ elif "max_tokens" in str(e):
312
+ print(f"\nError: {e}", file=sys.stderr)
313
+ print(f"Current max_tokens: {max_tokens}", file=sys.stderr)
314
+ print("Try increasing with: --max-tokens <higher_number>", file=sys.stderr)
315
+ sys.exit(3)
316
+
317
+ else:
318
+ print(f"Error: {e}", file=sys.stderr)
319
+ sys.exit(4)
320
+
321
+
322
+ if __name__ == "__main__":
323
+ main()