RohanAi commited on
Commit
8feda73
·
verified ·
1 Parent(s): 2f01d8f

Upload 11 files

Browse files
Files changed (11) hide show
  1. .env +7 -0
  2. Dockerfile +33 -0
  3. apis.py +164 -0
  4. config.py +85 -0
  5. main.py +175 -0
  6. openai_client.py +298 -0
  7. realtime_feedback.py +145 -0
  8. requirements.txt +24 -0
  9. test_openai_init.py +16 -0
  10. test_realtime_feedback.py +8 -0
  11. text_extractor.py +85 -0
.env ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Copy this to .env and fill in your actual values
2
+
3
+ # OpenAI API Configuration
4
+ OPENAI_API_KEY=sk-rHhP2-gXcfLHTJ8X6bL4pQ
5
+ OPENAI_MODEL=gpt-oss-120b
6
+ OPENAI_TEMPERATURE=0.7
7
+ OPENAI_MAX_TOKENS=80000
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 slim image
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy requirements first (for better caching)
8
+ COPY requirements.txt .
9
+
10
+ # Install Python dependencies
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy application code
14
+ COPY . .
15
+
16
+ # Create necessary directories
17
+ RUN mkdir -p generated_test_cases
18
+ RUN mkdir -p uploads
19
+ RUN mkdir -p inputFiles
20
+ RUN mkdir -p "Example Output Files"
21
+ RUN mkdir -p "Test Case Files"
22
+
23
+ # Set permissions
24
+ RUN chmod -R 755 /app
25
+
26
+ # Expose port
27
+ EXPOSE 7860
28
+
29
+ # Hugging Face Spaces expects the app to run on port 7860
30
+ ENV PORT=7860
31
+
32
+ # Run the application
33
+ CMD ["uvicorn", "apis:app", "--host", "0.0.0.0", "--port", "7860"]
apis.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, UploadFile, File
2
+ from fastapi.responses import PlainTextResponse, HTMLResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from datetime import datetime
6
+ from fastapi.responses import JSONResponse, RedirectResponse
7
+ import os
8
+ import tempfile
9
+ import shutil
10
+ import os,shutil,tempfile
11
+ from main import PCBTestCaseApp
12
+ from openai_client import TestCaseGenerator, HTMLGenerator
13
+
14
+ app = FastAPI(title="PCB Test Case Generator API")
15
+
16
+
17
+ # Add CORS middleware to allow frontend requests
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"], # In production, specify your domain
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ # Create static directory if it doesn't exist
27
+ os.makedirs("static", exist_ok=True)
28
+ os.makedirs("html_outputs", exist_ok=True)
29
+ # Mount static files
30
+ app.mount("/static", StaticFiles(directory="static"), name="static")
31
+ app.mount("/html-output", StaticFiles(directory="html_outputs"), name="html_outputs")
32
+
33
+ @app.get("/")
34
+ async def read_root():
35
+ """Redirect to static index.html"""
36
+ return RedirectResponse(url="/static/index.html")
37
+
38
+
39
+
40
+
41
+ @app.post("/generate-test-case")
42
+ async def generate_test_case(
43
+ manual_file: UploadFile = File(...),
44
+ ipc_file: UploadFile = File(...)
45
+ ):
46
+ """
47
+ Generate PCB test cases from uploaded files and return JSON with content for web rendering
48
+
49
+ Args:
50
+ manual_file: Manual document file (required)
51
+ ipc_file: IPC file (required)
52
+
53
+ Returns:
54
+ JSON response with test case content and metadata including HTML file path
55
+ """
56
+ temp_files = []
57
+
58
+ try:
59
+ # Create temporary directory for uploaded files
60
+ temp_dir = tempfile.mkdtemp()
61
+
62
+ # Save uploaded files temporarily
63
+ manual_path = os.path.join(temp_dir, manual_file.filename)
64
+ ipc_path = os.path.join(temp_dir, ipc_file.filename)
65
+
66
+ with open(manual_path, "wb") as buffer:
67
+ content = await manual_file.read()
68
+ buffer.write(content)
69
+ temp_files.append(manual_path)
70
+
71
+ with open(ipc_path, "wb") as buffer:
72
+ content = await ipc_file.read()
73
+ buffer.write(content)
74
+ temp_files.append(ipc_path)
75
+
76
+ # Initialize PCB Test Case App
77
+ pcb_app = PCBTestCaseApp()
78
+
79
+ # Fixed file paths (you can make these configurable)
80
+ sample_path = "Example Output Files/AE304196-001_LoRa Car Radio Bring-Up Procedure.docx"
81
+ d356_path = "Test Case Files/UNO-TH_Rev3e.d356"
82
+ bom_path = "Example Output Files/newModelBom.csv"
83
+
84
+ # Ensure output directory exists
85
+ output_dir = "generated_test_cases"
86
+ os.makedirs(output_dir, exist_ok=True)
87
+
88
+ # Get model name from config (from env file)
89
+ model_name = pcb_app.config.openai_model.replace("/", "_")
90
+
91
+ # Generate filename with current datetime and model name
92
+ timestamp = datetime.now().strftime("%d%m%y%H%M")
93
+ output_filename = f"{timestamp}_{model_name}.txt"
94
+ output_path = os.path.join(output_dir, output_filename)
95
+
96
+ # Generate test cases
97
+ test_cases = pcb_app.generate_test_cases(
98
+ manual_path,
99
+ sample_path,
100
+ ipc_path,
101
+ d356_path,
102
+ bom_path,
103
+ output_path
104
+ )
105
+
106
+ # Generate HTML output
107
+ output_dir_html = "html_outputs"
108
+ os.makedirs(output_dir_html, exist_ok=True)
109
+ print(f"=== GENERATING HTML OUTPUT ===")
110
+
111
+ generator = HTMLGenerator(pcb_app.config)
112
+ html_output_path = os.path.join(output_dir_html, f"{timestamp}_{model_name}.html")
113
+ generator.text_file_to_html(output_path, html_output_path)
114
+
115
+ # Read the generated test case content
116
+ try:
117
+ with open(output_path, 'r', encoding='utf-8') as f:
118
+ test_case_content = f.read()
119
+ except UnicodeDecodeError:
120
+ # Try with different encodings if utf-8 fails
121
+ try:
122
+ with open(output_path, 'r', encoding='latin-1') as f:
123
+ test_case_content = f.read()
124
+ except:
125
+ with open(output_path, 'r', encoding='cp1252') as f:
126
+ test_case_content = f.read()
127
+
128
+ # HTML file details
129
+ html_file_name = f"{timestamp}_{model_name}.html"
130
+ html_file_url = f"/html-output/{html_file_name}"
131
+
132
+ # Return JSON response with content and metadata
133
+ return JSONResponse(
134
+ content={
135
+ "success": True,
136
+ "content": test_case_content,
137
+ "filename": output_filename,
138
+ "timestamp": timestamp,
139
+ "model": model_name,
140
+ "html_file": html_file_name,
141
+ "html_url": html_file_url
142
+ }
143
+ )
144
+
145
+ except Exception as e:
146
+ print(f"Application error: {e}")
147
+ raise HTTPException(status_code=500, detail=f"Error generating test case: {str(e)}")
148
+
149
+ finally:
150
+ # Clean up temporary files
151
+ for temp_file in temp_files:
152
+ if os.path.exists(temp_file):
153
+ os.remove(temp_file)
154
+ # Remove temporary directory
155
+ if 'temp_dir' in locals() and os.path.exists(temp_dir):
156
+ shutil.rmtree(temp_dir)
157
+
158
+
159
+
160
+
161
+
162
+ if __name__ == "__main__":
163
+ import uvicorn
164
+ uvicorn.run(app, host="0.0.0.0", port=7860)
config.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration management for PCB Test Case Generator
3
+ Handles API keys and application settings
4
+ """
5
+ import os
6
+ from typing import Optional
7
+ from dataclasses import dataclass
8
+
9
+ from dotenv import load_dotenv
10
+
11
+
12
+ load_dotenv() # Load environment variables from .env file if present
13
+
14
+ @dataclass
15
+ class Config:
16
+ """Configuration class for storing application settings"""
17
+ openai_api_key: Optional[str] = None
18
+ openai_base_url: Optional[str] = "https://api.ai.it.ufl.edu"
19
+ openai_model: str = "gpt-oss-120b"
20
+ temperature: float = 0.7
21
+ max_tokens: Optional[int] = None
22
+
23
+ def __post_init__(self):
24
+ """Load configuration from environment variables"""
25
+ self.openai_api_key = os.getenv("OPENAI_API_KEY",self.openai_api_key)
26
+ print(self.openai_api_key)
27
+ # Optional proxy/base URL for OpenAI-compatible endpoints (e.g., LiteLLM proxy)
28
+ self.openai_base_url = os.getenv("OPENAI_BASE_URL", self.openai_base_url)
29
+ self.openai_model = os.getenv("OPENAI_MODEL", self.openai_model)
30
+
31
+ # Load temperature from env if set
32
+ temp_env = os.getenv("OPENAI_TEMPERATURE",self.temperature)
33
+ if temp_env:
34
+ try:
35
+ self.temperature = float(temp_env)
36
+ except ValueError:
37
+ print(f"Warning: Invalid temperature value '{temp_env}', using default {self.temperature}")
38
+
39
+ # Load max_tokens from env if set
40
+ tokens_env = os.getenv("OPENAI_MAX_TOKENS",self.max_tokens)
41
+ if tokens_env:
42
+ try:
43
+ self.max_tokens = int(tokens_env)
44
+ except ValueError:
45
+ print(f"Warning: Invalid max_tokens value '{tokens_env}', using default None")
46
+
47
+ def validate(self) -> bool:
48
+ """Validate that required configuration is present"""
49
+ if not self.openai_api_key:
50
+ print("ERROR: OpenAI API key not found. Please set OPENAI_API_KEY environment variable.")
51
+ return False
52
+ return True
53
+
54
+ @classmethod
55
+ def load_from_file(cls, config_file: str) -> 'Config':
56
+ """Load configuration from a file (optional implementation)"""
57
+ # This could be extended to load from JSON/YAML files
58
+ config = cls()
59
+
60
+ # Example of loading from a simple text file
61
+ if os.path.exists(config_file):
62
+ try:
63
+ with open(config_file, 'r') as f:
64
+ for line in f:
65
+ line = line.strip()
66
+ if '=' in line and not line.startswith('#'):
67
+ key, value = line.split('=', 1)
68
+ key, value = key.strip(), value.strip()
69
+
70
+ if key == 'OPENAI_API_KEY':
71
+ config.openai_api_key = value
72
+ elif key == 'OPENAI_MODEL':
73
+ config.openai_model = value
74
+ elif key == 'OPENAI_TEMPERATURE':
75
+ config.temperature = float(value)
76
+ elif key == 'OPENAI_MAX_TOKENS':
77
+ config.max_tokens = int(value)
78
+ except Exception as e:
79
+ print(f"Warning: Error loading config file {config_file}: {e}")
80
+
81
+ return config
82
+
83
+
84
+ # Global config instance
85
+ app_config = Config()
main.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main application file integrating document extraction and OpenAI API
3
+ """
4
+ import sys
5
+ from config import Config, app_config
6
+ from openai_client import TestCaseGenerator, HTMLGenerator
7
+ from realtime_feedback import RealTimeTestCaseGenerator
8
+ from text_extractor import extract_text_from_docx, extract_text_from_csv, summarize_ipc_or_d356_data
9
+ import os
10
+ from datetime import datetime
11
+
12
+
13
+ class PCBTestCaseApp:
14
+ """Main application class for PCB test case generation"""
15
+
16
+ def __init__(self, config_file: str = None):
17
+ """Initialize the application"""
18
+ if config_file:
19
+ self.config = Config.load_from_file(config_file)
20
+ else:
21
+ self.config = app_config
22
+
23
+ if not self.config.validate():
24
+ raise ValueError("Configuration validation failed")
25
+
26
+ self.test_generator = TestCaseGenerator(self.config)
27
+ print(f"PCB Test Case Generator initialized with model: {self.config.openai_model}")
28
+
29
+ def extract_documents(self, manual_path, sample_path, ipc_path, d356_path, bom_path):
30
+ """Extract content from all required documents"""
31
+ print("Extracting text from files...")
32
+
33
+ # Extract manual text
34
+ try:
35
+ manual_text = extract_text_from_docx(manual_path)
36
+ print(f"Extracted {len(manual_text)} characters from manual.")
37
+ except Exception as e:
38
+ manual_text = "ERROR: Could not read PCB Manual. Ensure file exists and is a valid .docx."
39
+ print(f"{manual_text} {e}")
40
+
41
+ # Extract sample output text
42
+ try:
43
+ sample_output_text = extract_text_from_docx(sample_path)
44
+ print(f"Extracted {len(sample_output_text)} characters from sample output.")
45
+ except Exception as e:
46
+ sample_output_text = "ERROR: Could not read Sample Output. Ensure file exists and is a valid .docx."
47
+ print(f"{sample_output_text} {e}")
48
+
49
+ # Generate summaries
50
+ ipc_summary = summarize_ipc_or_d356_data(ipc_path, 'ipc')
51
+ print(f"Generated IPC summary: {len(ipc_summary)} characters.")
52
+
53
+ d356_summary = summarize_ipc_or_d356_data(d356_path, 'd356')
54
+ print(f"Generated D356 summary: {len(d356_summary)} characters.")
55
+
56
+ # Extract BOM text
57
+ try:
58
+ bom_text = extract_text_from_csv(bom_path)
59
+ print(f"Extracted {len(bom_text)} characters from BOM.")
60
+ except Exception as e:
61
+ bom_text = "ERROR: Could not read BOM. Ensure file exists and is a valid .csv."
62
+ print(f"{bom_text} {e}")
63
+
64
+ return manual_text, sample_output_text, ipc_summary, d356_summary, bom_text
65
+
66
+ def generate_test_cases(self, manual_path, sample_path, ipc_path, d356_path, bom_path,
67
+ output_path=None, preview_only=False):
68
+ """Generate PCB test cases from input documents"""
69
+ try:
70
+ # Extract documents
71
+ manual_text, sample_output_text, ipc_summary, d356_summary, bom_text = \
72
+ self.extract_documents(manual_path, sample_path, ipc_path, d356_path, bom_path)
73
+
74
+ # Generate or preview
75
+ if preview_only:
76
+ print("\n=== PREVIEW MODE ===")
77
+ result = self.test_generator.generate(
78
+ manual_text, sample_output_text, ipc_summary, d356_summary, bom_text,
79
+ preview_only=True
80
+ )
81
+ print(result)
82
+ return result
83
+ else:
84
+ print("\n=== GENERATING TEST CASES ===")
85
+ result = self.test_generator.generate(
86
+ manual_text, sample_output_text, ipc_summary, d356_summary, bom_text
87
+ )
88
+
89
+ if output_path:
90
+ with open(output_path, 'w', encoding='utf-8') as f:
91
+ f.write(result)
92
+ print(f"Test cases saved to: {output_path}")
93
+
94
+ return result
95
+
96
+ except Exception as e:
97
+ print(f"Error generating test cases: {e}")
98
+ raise
99
+
100
+
101
+ def main():
102
+ """Main entry point"""
103
+ try:
104
+ app = PCBTestCaseApp()
105
+
106
+ # File paths from your existing code
107
+ manual_path = "inputFiles/Clemson_HW_Spec_V4_092325.docx"
108
+ sample_path = "Example Output Files/AE304196-001_LoRa Car Radio Bring-Up Procedure.docx"
109
+ ipc_path = "inputFiles/Assembly Testpoint Report for Car-PCB1.ipc"
110
+ d356_path = "Test Case Files/UNO-TH_Rev3e.d356"
111
+ bom_path = "Example Output Files/newModelBom.csv"
112
+ # Ensure output directory exists
113
+ output_dir = "generated_test_cases"
114
+ os.makedirs(output_dir, exist_ok=True)
115
+
116
+ # Get model name from config (from env file)
117
+ model_name = app.config.openai_model.replace("/", "_")
118
+
119
+ # Generate filename with current datetime and model name
120
+ timestamp = datetime.now().strftime("%d%m%y%H%M")
121
+ output_filename = f"{timestamp}_{model_name}.txt"
122
+ output_path = os.path.join(output_dir, output_filename)
123
+
124
+
125
+ preview_mode = "--preview" in sys.argv
126
+ stream_mode = "--stream" in sys.argv
127
+
128
+ if preview_mode:
129
+ app.generate_test_cases(manual_path, sample_path, ipc_path, d356_path, bom_path, preview_only=True)
130
+ elif stream_mode:
131
+ # Use the realtime streamer: extract documents first, then stream generation
132
+ manual_text, sample_output_text, ipc_summary, d356_summary, bom_text = \
133
+ app.extract_documents(manual_path, sample_path, ipc_path, d356_path, bom_path)
134
+
135
+ streamer = RealTimeTestCaseGenerator(app.config)
136
+
137
+ # Open the output file for incremental writes
138
+ with open(output_path, 'w', encoding='utf-8') as out_f:
139
+ def on_chunk(chunk: str):
140
+ # Print to stdout and write to the file as chunks arrive
141
+ try:
142
+ print(chunk, end='')
143
+ except Exception:
144
+ pass
145
+ try:
146
+ out_f.write(chunk)
147
+ out_f.flush()
148
+ except Exception:
149
+ pass
150
+
151
+ full_text = streamer.stream_generate(
152
+ manual_text, sample_output_text, ipc_summary, d356_summary, bom_text,
153
+ on_chunk=on_chunk
154
+ )
155
+
156
+ print(f"\nStreaming complete — full output saved to: {output_path}")
157
+ else:
158
+ test_cases = app.generate_test_cases(manual_path, sample_path, ipc_path, d356_path, bom_path, output_path)
159
+ print(f"\n=== GENERATED TEST CASES PREVIEW ===")
160
+ print(test_cases[:500] + "..." if len(test_cases) > 500 else test_cases)
161
+
162
+ except Exception as e:
163
+ print(f"Application error: {e}")
164
+ sys.exit(1)
165
+
166
+ output_dir_html = "html_outputs"
167
+ os.makedirs(output_dir_html, exist_ok=True)
168
+ print(f"=== GENERATING HTML OUTPUT ===")
169
+ generator = HTMLGenerator(app.config)
170
+ html_output_path = os.path.join(output_dir_html, f"{timestamp}_{model_name}.html")
171
+ generator.text_file_to_html(output_path, html_output_path)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()
openai_client.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Optional, Any
2
+ from pathlib import Path
3
+ import openai
4
+ from config import Config
5
+
6
+
7
+ class OpenAIClient:
8
+ """OpenAI API client with error handling and retry logic"""
9
+
10
+ def __init__(self, config: Config):
11
+ """
12
+ Initialize OpenAI client with configuration
13
+
14
+ Args:
15
+ config: Configuration object containing API settings
16
+ """
17
+ self.config = config
18
+ self.client = None
19
+ self._initialize_client()
20
+
21
+ def _initialize_client(self) -> None:
22
+ """Initialize the OpenAI client"""
23
+ if not self.config.validate():
24
+ raise ValueError("Invalid configuration: missing API key")
25
+
26
+ try:
27
+ # Use the new-style OpenAI client constructor which accepts api_key and base_url
28
+ client_kwargs = {"api_key": self.config.openai_api_key}
29
+ if getattr(self.config, "openai_base_url", None):
30
+ client_kwargs["base_url"] = self.config.openai_base_url
31
+
32
+ self.client = openai.OpenAI(**client_kwargs)
33
+ print(f"OpenAI client initialized with model: {self.config.openai_model}")
34
+ except Exception as e:
35
+ raise RuntimeError(f"Failed to initialize OpenAI client: {e}")
36
+
37
+ def generate_test_cases(self,
38
+ manual_text: str,
39
+ sample_output_text: str,
40
+ ipc_summary: str,
41
+ d356_summary: str,
42
+ bom_text: str) -> str:
43
+ """
44
+ Generate PCB test cases using OpenAI API
45
+
46
+ Args:
47
+ manual_text: PCB testing manual content
48
+ sample_output_text: Sample test report format
49
+ ipc_summary: IPC design summary
50
+ d356_summary: D356 test data summary
51
+ bom_text: Bill of materials text
52
+
53
+ Returns:
54
+ Generated test cases as string
55
+
56
+ Raises:
57
+ RuntimeError: If API call fails
58
+ """
59
+ try:
60
+ messages = self._prepare_messages(
61
+ manual_text, sample_output_text, ipc_summary, d356_summary, bom_text
62
+ )
63
+
64
+ response = self._make_api_call(messages)
65
+ return self._extract_response_content(response)
66
+
67
+ except Exception as e:
68
+ raise RuntimeError(f"Failed to generate test cases: {e}")
69
+
70
+ def _prepare_messages(self,
71
+ manual_text: str,
72
+ sample_output_text: str,
73
+ ipc_summary: str,
74
+ d356_summary: str,
75
+ bom_text: str) -> List[Dict[str, str]]:
76
+ """Prepare messages for OpenAI API call"""
77
+
78
+ system_message = {
79
+ "role": "system",
80
+ "content": (
81
+ "You are an expert PCB test engineer. Your task is to generate a comprehensive set of test cases "
82
+ "for a new PCB design. You must follow the testing methodologies and safety guidelines provided "
83
+ "in the 'PCB Manual' and structure the output strictly according to the 'Sample Test Report Format'. "
84
+ "The test cases should cover visual inspection, electrical tests, and functional tests relevant "
85
+ "to the components and design described. Think step-by-step to cover all critical aspects."
86
+ )
87
+ }
88
+
89
+ user_message_content = f"""
90
+ ### PCB Manual for Reference:
91
+ {manual_text}
92
+
93
+ ### Sample Test Report Format (Guidance for Structure and Content):
94
+ {sample_output_text}
95
+
96
+ ### New Device Design Data (IPC Summary):
97
+ This section provides a high-level overview of the new PCB's design.
98
+ {ipc_summary}
99
+
100
+ ### New Device Test Data (D356 Summary):
101
+ This section provides details about specific test points and electrical properties of the new PCB.
102
+ {d356_summary}
103
+
104
+ ### New Device Bill of Materials (BOM):
105
+ This is the list of components on the new PCB.
106
+ {bom_text}
107
+
108
+ ---
109
+ Based on the 'PCB Manual', the 'Sample Test Report Format', and the details of the 'New Device' (from IPC summary, D356 summary, and BOM), please generate a detailed list of test cases.
110
+
111
+ Ensure the test cases are presented in the exact format demonstrated in the 'Sample Test Report Format', including headings, table structure, and level of detail. Focus on:
112
+
113
+ 1. **Visual Inspection:** Component orientation, solder quality, silkscreen errors.
114
+ 2. **Electrical Continuity & Shorts:** Critical power and ground nets, key signal lines.
115
+ 3. **Power System Tests:** Voltage rails, current draw.
116
+ 4. **Functional Tests:** Based on the components (e.g., if U1 is an MCU, test its I/O, if J1 is USB, test USB connectivity).
117
+ 5. **Environmental/Stress Tests:** If specified in the manual (e.g., temperature cycling - provide placeholders if data is not available).
118
+
119
+ For each test case, provide a unique ID, a clear description, expected results, and leave space for 'Actual Result' and 'Pass/Fail'.
120
+ """
121
+
122
+ user_message = {
123
+ "role": "user",
124
+ "content": user_message_content
125
+ }
126
+
127
+ return [system_message, user_message]
128
+
129
+ def _make_api_call(self, messages: List[Dict[str, str]]) -> Any:
130
+ """Make the actual API call to OpenAI"""
131
+
132
+ # Prepare API call parameters
133
+ api_params = {
134
+ "model": self.config.openai_model,
135
+ "messages": messages,
136
+ "temperature": self.config.temperature
137
+ }
138
+
139
+ # Add max_tokens if specified
140
+ if self.config.max_tokens:
141
+ api_params["max_tokens"] = self.config.max_tokens
142
+
143
+ try:
144
+ # Using the new client instance to create a chat completion via the proxy-compatible API
145
+ response = self.client.chat.completions.create(**api_params)
146
+ return response
147
+ except Exception as e:
148
+ # Generic catch — the new OpenAI SDK raises different exception types depending on the transport
149
+ raise RuntimeError(f"OpenAI API call failed: {e}")
150
+
151
+ def _extract_response_content(self, response: Any) -> str:
152
+ """Extract content from OpenAI API response"""
153
+ try:
154
+ return response.choices[0].message.content
155
+ except (AttributeError, IndexError, KeyError) as e:
156
+ raise RuntimeError(f"Failed to extract response content: {e}")
157
+
158
+ def get_prompt_info(self,
159
+ manual_text: str,
160
+ sample_output_text: str,
161
+ ipc_summary: str,
162
+ d356_summary: str,
163
+ bom_text: str) -> Dict[str, Any]:
164
+ """
165
+ Get information about the prompt without making an API call
166
+
167
+ Returns:
168
+ Dictionary with prompt information including token estimates
169
+ """
170
+ messages = self._prepare_messages(
171
+ manual_text, sample_output_text, ipc_summary, d356_summary, bom_text
172
+ )
173
+
174
+ system_content = messages[0]["content"]
175
+ user_content = messages[1]["content"]
176
+
177
+ # Rough token estimation (1 token ≈ 4 characters for English text)
178
+ total_chars = len(system_content) + len(user_content)
179
+ estimated_tokens = total_chars // 4
180
+
181
+ return {
182
+ "system_message": system_content,
183
+ "user_message_preview": user_content[:500] + "..." if len(user_content) > 500 else user_content,
184
+ "total_characters": total_chars,
185
+ "estimated_tokens": estimated_tokens,
186
+ "model": self.config.openai_model,
187
+ "temperature": self.config.temperature
188
+ }
189
+
190
+
191
+ class TestCaseGenerator:
192
+ """High-level interface for generating PCB test cases"""
193
+
194
+ def __init__(self, config: Config):
195
+ """Initialize with configuration"""
196
+ self.config = config
197
+ self.openai_client = OpenAIClient(config)
198
+
199
+ def generate(self,
200
+ manual_text: str,
201
+ sample_output_text: str,
202
+ ipc_summary: str,
203
+ d356_summary: str,
204
+ bom_text: str,
205
+ preview_only: bool = False) -> str:
206
+ """
207
+ Generate test cases or preview prompt
208
+
209
+ Args:
210
+ manual_text: PCB testing manual content
211
+ sample_output_text: Sample test report format
212
+ ipc_summary: IPC design summary
213
+ d356_summary: D356 test data summary
214
+ bom_text: Bill of materials text
215
+ preview_only: If True, return prompt info instead of generating
216
+
217
+ Returns:
218
+ Generated test cases or prompt information
219
+ """
220
+ if preview_only:
221
+ prompt_info = self.openai_client.get_prompt_info(
222
+ manual_text, sample_output_text, ipc_summary, d356_summary, bom_text
223
+ )
224
+
225
+ return f"""
226
+ --- Generated Prompt Structure (for review) ---
227
+ System Message:
228
+ {prompt_info['system_message']}
229
+
230
+ User Message (preview):
231
+ {prompt_info['user_message_preview']}
232
+
233
+ Total characters: {prompt_info['total_characters']}
234
+ Estimated tokens: {prompt_info['estimated_tokens']}
235
+ Model: {prompt_info['model']}
236
+ Temperature: {prompt_info['temperature']}
237
+ """
238
+ else:
239
+ return self.openai_client.generate_test_cases(
240
+ manual_text, sample_output_text, ipc_summary, d356_summary, bom_text
241
+ )
242
+
243
+ class HTMLGenerator:
244
+ """Class to convert a text file to HTML using OpenAI API and save the result."""
245
+ def __init__(self, config: Config):
246
+ self.config = config
247
+ self.openai_client = OpenAIClient(config)
248
+
249
+ def text_file_to_html(self, input_txt_path: str, output_html_path: str):
250
+ """
251
+ Convert a text file to HTML using OpenAI API and save the result.
252
+ Args:
253
+ input_txt_path: Path to the input text file.
254
+ output_html_path: Path to save the output HTML file.
255
+ Returns:
256
+ The generated HTML content as a string.
257
+ Raises:
258
+ RuntimeError: If API call or file operations fail.
259
+ """
260
+ try:
261
+ with open(input_txt_path, 'r', encoding='utf-8') as f:
262
+ text_content = f.read()
263
+ except Exception as e:
264
+ raise RuntimeError(f"Failed to read input file: {e}")
265
+
266
+ # Prepare prompt for HTML conversion
267
+ messages = [
268
+ {
269
+ "role": "system",
270
+ "content": """You are a expert HTML and CSS webpage designer.
271
+ Convert the following plain text into a clean, well‑structured
272
+ HTML page with styled CSS. Add colours and fonts to enhance readability.
273
+ Keep headings, paragraphs, lists, and add basic styling so
274
+ the page looks presentable in a browser.
275
+ DO NOT OUTPUT ANYTHING ELSE OTHER THAN WHAT IS STATED, OTHER WISE YOU WILL BE PENALIZED"""
276
+ },
277
+ {
278
+ "role": "user",
279
+ "content": f"Convert the following text to HTML and style.\n\n{text_content}"
280
+ }
281
+ ]
282
+
283
+ try:
284
+ response = self.openai_client._make_api_call(messages)
285
+ html_content = self.openai_client._extract_response_content(response)
286
+ except Exception as e:
287
+ raise RuntimeError(f"OpenAI API call failed: {e}")
288
+
289
+ try:
290
+ with open(output_html_path, 'w', encoding='utf-8') as f:
291
+ f.write(html_content)
292
+ except Exception as e:
293
+ raise RuntimeError(f"Failed to write HTML file: {e}")
294
+
295
+ """
296
+ OpenAI API client for PCB test case generation
297
+ Handles all OpenAI API interactions with proper error handling
298
+ """
realtime_feedback.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Realtime feedback / streaming helper for test-case generation.
3
+
4
+ This module provides a separate class to perform streamed chat completions
5
+ and deliver incremental chunks to a caller-provided callback. It is kept
6
+ separate from `openai_client.py` so existing code paths remain unchanged.
7
+
8
+ Usage (non-blocking design):
9
+ from config import Config
10
+ from realtime_feedback import RealTimeTestCaseGenerator
11
+
12
+ cfg = Config(openai_api_key='sk-xxx', openai_base_url='https://api.ai.it.ufl.edu')
13
+ streamer = RealTimeTestCaseGenerator(cfg)
14
+
15
+ def on_chunk(chunk: str):
16
+ print(chunk, end='')
17
+
18
+ full_text = streamer.stream_generate(manual_text, sample_text, ipc_summary, d356_summary, bom_text, on_chunk=on_chunk)
19
+ print('\n--- done ---')
20
+
21
+ The implementation tries to be robust to different SDK streaming event shapes
22
+ and will call the `on_chunk` callback for each incoming text delta. It also
23
+ returns the full assembled text once streaming completes.
24
+ """
25
+ from typing import Callable, Optional, Any, List, Dict
26
+ from openai_client import OpenAIClient
27
+ from config import Config
28
+
29
+
30
+ class RealTimeTestCaseGenerator:
31
+ """Provides a streaming interface for test-case generation."""
32
+
33
+ def __init__(self, config: Config):
34
+ """Create a streamer using the same Config as other components.
35
+
36
+ The class internally uses `OpenAIClient` to obtain a configured
37
+ `openai.OpenAI` client instance, so auth/proxy settings remain
38
+ consistent with the rest of the application.
39
+ """
40
+ self.config = config
41
+ self.openai_client = OpenAIClient(config)
42
+
43
+ def _build_api_params(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
44
+ params: Dict[str, Any] = {
45
+ "model": self.config.openai_model,
46
+ "messages": messages,
47
+ "temperature": self.config.temperature,
48
+ "stream": True,
49
+ }
50
+ if self.config.max_tokens:
51
+ params["max_tokens"] = self.config.max_tokens
52
+ return params
53
+
54
+ def stream_generate(self,
55
+ manual_text: str,
56
+ sample_output_text: str,
57
+ ipc_summary: str,
58
+ d356_summary: str,
59
+ bom_text: str,
60
+ on_chunk: Optional[Callable[[str], None]] = None) -> str:
61
+ """Stream generation from the model and call `on_chunk` for each delta.
62
+
63
+ Returns the assembled full text once streaming completes.
64
+ """
65
+ # Reuse the same prompt construction as OpenAIClient
66
+ # Build the messages payload here (kept minimal — mirror _prepare_messages)
67
+ system_message = {
68
+ "role": "system",
69
+ "content": (
70
+ "You are an expert PCB test engineer. Your task is to generate a comprehensive set of test cases "
71
+ "for a new PCB design. Follow instructions and produce step-by-step test cases."
72
+ )
73
+ }
74
+
75
+ user_message_content = f"""
76
+ ### PCB Manual for Reference:
77
+ {manual_text}
78
+
79
+ ### Sample Test Report Format (Guidance for Structure and Content):
80
+ {sample_output_text}
81
+
82
+ ### New Device Design Data (IPC Summary):
83
+ {ipc_summary}
84
+
85
+ ### New Device Test Data (D356 Summary):
86
+ {d356_summary}
87
+
88
+ ### New Device Bill of Materials (BOM):
89
+ {bom_text}
90
+
91
+ Please stream the generated test cases in the same format as the sample.
92
+ """
93
+
94
+ user_message = {"role": "user", "content": user_message_content}
95
+ messages = [system_message, user_message]
96
+
97
+ api_params = self._build_api_params(messages)
98
+
99
+ full_text = ""
100
+
101
+ # Create the streaming iterator from the configured client
102
+ try:
103
+ stream_iterable = self.openai_client.client.chat.completions.create(**api_params)
104
+ except Exception as e:
105
+ raise RuntimeError(f"Failed to start streaming API call: {e}")
106
+
107
+ # Iterate over streaming events. Different transports/SDK versions
108
+ # may yield slightly different shapes; be defensive when extracting text.
109
+ for event in stream_iterable:
110
+ chunk = None
111
+ # Try dict-like access first
112
+ try:
113
+ # event could be a mapping
114
+ choice = event.get("choices", [None])[0] if isinstance(event, dict) else None
115
+ if choice:
116
+ delta = choice.get("delta") or choice.get("message") or {}
117
+ if isinstance(delta, dict):
118
+ chunk = delta.get("content") or delta.get("text")
119
+ except Exception:
120
+ chunk = None
121
+
122
+ # Try attribute-style access
123
+ if not chunk:
124
+ try:
125
+ choice = getattr(event, "choices", None)
126
+ if choice:
127
+ c0 = choice[0]
128
+ delta = getattr(c0, "delta", None) or getattr(c0, "message", None)
129
+ if hasattr(delta, "get"):
130
+ chunk = delta.get("content") or delta.get("text")
131
+ else:
132
+ chunk = getattr(delta, "content", None)
133
+ except Exception:
134
+ chunk = None
135
+
136
+ if chunk:
137
+ full_text += chunk
138
+ if on_chunk:
139
+ try:
140
+ on_chunk(chunk)
141
+ except Exception:
142
+ # Don't let callback errors stop streaming
143
+ pass
144
+
145
+ return full_text
requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ anyio==4.11.0
3
+ certifi==2025.8.3
4
+ colorama==0.4.6
5
+ distro==1.9.0
6
+ h11==0.16.0
7
+ httpcore==1.0.9
8
+ httpx==0.28.1
9
+ idna==3.10
10
+ jiter==0.11.0
11
+ lxml==6.0.2
12
+ openai==1.109.1
13
+ pillow==11.3.0
14
+ pydantic==2.11.9
15
+ pydantic_core==2.33.2
16
+ python-docx==1.2.0
17
+ sniffio==1.3.1
18
+ tqdm==4.67.1
19
+ typing-inspection==0.4.1
20
+ typing_extensions==4.15.0
21
+ fastapi
22
+ uvicorn[standard]
23
+ python-multipart
24
+ dotenv
test_openai_init.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import openai
2
+
3
+ print('openai module file =', getattr(openai, '__file__', 'builtin'))
4
+
5
+ # Create a client using the new constructor style. Use a dummy API key (do not print it).
6
+ client = openai.OpenAI( base_url='https://api.ai.it.ufl.edu')
7
+ print('client created:', type(client))
8
+
9
+ # Sanity-check attributes exist (don't call network)
10
+ has_chat = hasattr(client, 'chat')
11
+ print('client has chat attribute:', has_chat)
12
+ try:
13
+ has_completions = hasattr(client.chat, 'completions')
14
+ print('client.chat has completions attribute:', has_completions)
15
+ except Exception as e:
16
+ print('Could not introspect client.chat:', e)
test_realtime_feedback.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from config import Config
2
+ from realtime_feedback import RealTimeTestCaseGenerator
3
+
4
+ cfg = Config(openai_api_key='test-key', openai_base_url='https://api.ai.it.ufl.edu')
5
+ print('Config OK', cfg.openai_api_key, cfg.openai_base_url)
6
+
7
+ streamer = RealTimeTestCaseGenerator(cfg)
8
+ print('Streamer created:', streamer)
text_extractor.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import docx
2
+ import csv
3
+ import io # Used for handling string as file-like object for IPC/D356 summaries
4
+
5
+ def extract_text_from_docx(filepath):
6
+ """Extracts all text from a .docx file."""
7
+ doc = docx.Document(filepath)
8
+ full_text = []
9
+ for para in doc.paragraphs:
10
+ full_text.append(para.text)
11
+ return "\n".join(full_text)
12
+
13
+ def extract_text_from_csv(filepath):
14
+ """Extracts and formats text from a CSV file."""
15
+ with open(filepath, 'r', encoding='utf-8') as f:
16
+ reader = csv.reader(f)
17
+ header = next(reader) # Read header
18
+ csv_data = [", ".join(header)] # Start with header
19
+ for row in reader:
20
+ csv_data.append(", ".join(row))
21
+ return "\n".join(csv_data)
22
+
23
+ def summarize_ipc_or_d356_data(filepath, file_type):
24
+ """
25
+ Placeholder for summarizing IPC or D356 data.
26
+ In a real application, this would involve a dedicated parser.
27
+ For demonstration, we'll return a placeholder string.
28
+ """
29
+ if file_type == 'ipc':
30
+ # Imagine complex parsing here, identifying ICs, connectors, critical nets etc.
31
+ # For now, it's a conceptual summary.
32
+ return f"Summary of {filepath} (IPC design data):\n" \
33
+ "- Identifies major components like U1 (MCU), U2 (Power IC), J1 (USB connector).\n" \
34
+ "- Provides connectivity information for power rails (VCC, GND) and data lines (e.g., I2C on U1 pins 3,4).\n" \
35
+ "- Indicates board dimensions and layer stackup.\n" \
36
+ "- Critical components: U1 (Microcontroller), U2 (Voltage Regulator), Q1 (MOSFET)."
37
+ elif file_type == 'd356':
38
+ # Imagine parsing test points, nets to check, etc.
39
+ return f"Summary of {filepath} (D356 test data):\n" \
40
+ "- Lists test points: TP1 (VCC_3V3), TP2 (GND), TP3 (U1_I2C_SDA).\n" \
41
+ "- Indicates net connectivity for electrical tests.\n" \
42
+ "- Specifies areas for visual inspection related to component placement."
43
+ else:
44
+ return f"Could not summarize unknown file type: {filepath}"
45
+
46
+
47
+ # --- File Paths (Update these to your actual file paths) ---
48
+ manual_path = "inputFiles/Clemson_HW_Spec_V4_092325.docx"
49
+ sample_output_path = "Example Output Files/AE304196-001_LoRa Car Radio Bring-Up Procedure.docx"
50
+ ipc_design_path = "inputFiles/Assembly Testpoint Report for Car-PCB1.ipc" # This is conceptual for direct LLM input
51
+
52
+ # Test Data (for the new device you want to generate test cases for)
53
+ new_device_d356_path = "Test Case Files/UNO-TH_Rev3e.d356" # This is conceptual for direct LLM input
54
+ new_device_bom_path = "Example Output Files/newModelBom.csv"
55
+
56
+ # --- 1. Extract Text from Existing Files ---
57
+ print("Extracting text from files...")
58
+ try:
59
+ manual_text = extract_text_from_docx(manual_path)
60
+ print(f"Extracted {len(manual_text)} characters from manual.")
61
+ except Exception as e:
62
+ manual_text = "ERROR: Could not read PCB Manual. Ensure file exists and is a valid .docx."
63
+ print(manual_text, e)
64
+
65
+ try:
66
+ sample_output_text = extract_text_from_docx(sample_output_path)
67
+ print(f"Extracted {len(sample_output_text)} characters from sample output.")
68
+ except Exception as e:
69
+ sample_output_text = "ERROR: Could not read Sample Output. Ensure file exists and is a valid .docx."
70
+ print(sample_output_text, e)
71
+
72
+ # Summarize IPC and D356 (conceptual for LLM input)
73
+ # In a real scenario, you'd run external tools or parsers here.
74
+ ipc_summary = summarize_ipc_or_d356_data(ipc_design_path, 'ipc')
75
+ print(f"Generated IPC summary (conceptual): {len(ipc_summary)} characters.")
76
+
77
+ d356_summary = summarize_ipc_or_d356_data(new_device_d356_path, 'd356')
78
+ print(f"Generated D356 summary (conceptual): {len(d356_summary)} characters.")
79
+
80
+ try:
81
+ new_device_bom_text = extract_text_from_csv(new_device_bom_path)
82
+ print(f"Extracted {len(new_device_bom_text)} characters from new device BOM.")
83
+ except Exception as e:
84
+ new_device_bom_text = "ERROR: Could not read New Device BOM. Ensure file exists and is a valid .csv."
85
+ print(new_device_bom_text, e)