structai 0.1.0__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.
structai/mp.py ADDED
@@ -0,0 +1,96 @@
1
+ import concurrent.futures
2
+ from tqdm import tqdm
3
+
4
+ def multi_thread(inp_list, function, max_workers=40, use_tqdm=True):
5
+ results = [None] * len(inp_list) # Initialize results list
6
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
7
+ future_to_index = {
8
+ executor.submit(function, **item): index # Unpack item
9
+ for index, item in enumerate(inp_list)
10
+ }
11
+
12
+ futures_iterator = concurrent.futures.as_completed(future_to_index)
13
+ if use_tqdm:
14
+ futures_iterator = tqdm(futures_iterator, total=len(future_to_index))
15
+
16
+ for future in futures_iterator:
17
+ index = future_to_index[future]
18
+ try:
19
+ result = future.result()
20
+ results[index] = result # Place result in correct position
21
+ except Exception as e:
22
+ print(f"Error processing item {inp_list[index]}: {str(e)}")
23
+
24
+ return results
25
+
26
+ def multi_process(inp_list, function, max_workers=40, use_tqdm=True):
27
+ results = [None] * len(inp_list) # Initialize results list
28
+ with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
29
+ future_to_index = {
30
+ executor.submit(function, **item): index # Unpack item
31
+ for index, item in enumerate(inp_list)
32
+ }
33
+
34
+ futures_iterator = concurrent.futures.as_completed(future_to_index)
35
+ if use_tqdm:
36
+ futures_iterator = tqdm(futures_iterator, total=len(future_to_index))
37
+
38
+ for future in futures_iterator:
39
+ index = future_to_index[future]
40
+ try:
41
+ result = future.result()
42
+ results[index] = result # Place result in correct position
43
+ except Exception as e:
44
+ print(f"Error processing item {inp_list[index]}: {str(e)}")
45
+
46
+ return results
47
+
48
+ if __name__ == '__main__':
49
+ # python -m structai.mp
50
+ print("Testing mp.py...")
51
+ import time
52
+
53
+ def process(a, b):
54
+ time.sleep(0.1)
55
+ return a + b
56
+
57
+ # Test data
58
+ inp = []
59
+ for a in range(10):
60
+ inp.append({'a': a, 'b': 100})
61
+
62
+ # Test multi_thread
63
+ print("Testing multi_thread...")
64
+ results = multi_thread(inp, process, max_workers=5)
65
+ expected = [a + 100 for a in range(10)]
66
+ assert results == expected, f"[===ERROR===][structai][mp.py][main] multi_thread failed: {results} != {expected}"
67
+ print("multi_thread passed")
68
+
69
+ # Test multi_process
70
+ print("Testing multi_process...")
71
+ results = multi_process(inp, process, max_workers=5)
72
+ assert results == expected, f"[===ERROR===][structai][mp.py][main] multi_process failed: {results} != {expected}"
73
+ print("multi_process passed")
74
+
75
+ # Test with torch if available
76
+ try:
77
+ import torch
78
+ if torch.cuda.is_available():
79
+ print("Testing with CUDA tensors...")
80
+ inp_cuda = []
81
+ for a in range(5):
82
+ inp_cuda.append({'a': torch.tensor(a).cuda(), 'b': torch.tensor(100).cuda()})
83
+
84
+ # Note: multi_process with CUDA tensors might be tricky due to pickling/context
85
+ # We'll just test multi_thread here as it's safer for shared CUDA tensors in simple scripts
86
+ results = multi_thread(inp_cuda, process, max_workers=2)
87
+ results_cpu = [r.item() for r in results]
88
+ expected_cpu = [a + 100 for a in range(5)]
89
+ assert results_cpu == expected_cpu, f"[===ERROR===][structai][mp.py][main] CUDA multi_thread failed: {results_cpu} != {expected_cpu}"
90
+ print("CUDA multi_thread passed")
91
+ else:
92
+ print("Skipping CUDA tests: CUDA not available")
93
+ except ImportError:
94
+ print("Skipping torch tests: torch not installed")
95
+
96
+ print("mp.py tests completed.")
@@ -0,0 +1,79 @@
1
+ import os
2
+
3
+ # =========================
4
+ # Config
5
+ # =========================
6
+
7
+ LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "") # Do not include /v1
8
+ LLM_API_KEY = os.environ.get("LLM_API_KEY", "")
9
+
10
+ def run_server(host="0.0.0.0", port=8001):
11
+ """
12
+ Run the OpenAI proxy server.
13
+ """
14
+ from fastapi import FastAPI, Request, Response
15
+ import httpx
16
+ import uvicorn
17
+
18
+ # =========================
19
+ # App
20
+ # =========================
21
+
22
+ app = FastAPI()
23
+
24
+ # =========================
25
+ # Universal OpenAI Proxy
26
+ # =========================
27
+
28
+ @app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
29
+ async def openai_proxy(path: str, request: Request):
30
+ """
31
+ Universal OpenAI-compatible proxy.
32
+ Supports chat.completions, responses, models, embeddings, etc.
33
+ """
34
+
35
+ # Target URL
36
+ target_url = f"{LLM_BASE_URL}/v1/{path}"
37
+
38
+ # Copy headers
39
+ headers = dict(request.headers)
40
+
41
+ # Override authorization
42
+ headers["authorization"] = f"Bearer {LLM_API_KEY}"
43
+ headers.pop("host", None)
44
+
45
+ # Read body
46
+ body = await request.body()
47
+
48
+ # Forward request
49
+ async with httpx.AsyncClient(timeout=300) as client:
50
+ upstream_resp = await client.request(
51
+ method=request.method,
52
+ url=target_url,
53
+ headers=headers,
54
+ content=body,
55
+ params=request.query_params,
56
+ )
57
+
58
+ # -------------------------
59
+ # IMPORTANT: header cleanup
60
+ # -------------------------
61
+ resp_headers = dict(upstream_resp.headers)
62
+
63
+ # httpx already decompressed the body
64
+ # Do NOT forward these headers
65
+ resp_headers.pop("content-encoding", None)
66
+ resp_headers.pop("content-length", None)
67
+
68
+ return Response(
69
+ content=upstream_resp.content,
70
+ status_code=upstream_resp.status_code,
71
+ headers=resp_headers,
72
+ media_type=resp_headers.get("content-type"),
73
+ )
74
+
75
+ uvicorn.run(app, host=host, port=port)
76
+
77
+ if __name__ == "__main__":
78
+ # python -m structai.openai_server
79
+ run_server()
structai/utils.py ADDED
@@ -0,0 +1,209 @@
1
+ from functools import wraps
2
+ import threading
3
+ import os
4
+
5
+
6
+ def run_with_timeout(func, args=(), kwargs=None, timeout=None):
7
+ """
8
+ Run a function with a timeout limit.
9
+
10
+ Args:
11
+ func (callable): The function to run.
12
+ args (tuple): Positional arguments for the function.
13
+ kwargs (dict): Keyword arguments for the function.
14
+ timeout (float or None): Maximum allowed execution time in seconds.
15
+
16
+ Returns:
17
+ The function's return value.
18
+
19
+ Raises:
20
+ TimeoutError: If execution exceeds the allowed time.
21
+ Exception: Any exception raised by the function.
22
+ """
23
+ if kwargs is None:
24
+ kwargs = {}
25
+
26
+ if timeout is None:
27
+ return func(*args, **kwargs)
28
+
29
+ result = [None]
30
+ exc = [None]
31
+
32
+ def target():
33
+ try:
34
+ result[0] = func(*args, **kwargs)
35
+ except Exception as e:
36
+ exc[0] = e
37
+
38
+ thread = threading.Thread(target=target)
39
+ thread.start()
40
+ thread.join(timeout)
41
+
42
+ if thread.is_alive():
43
+ raise TimeoutError(f"[{str(func.__name__)} timeout after {timeout}s]")
44
+
45
+ if exc[0] is not None:
46
+ raise exc[0]
47
+
48
+ return result[0]
49
+
50
+
51
+ def timeout_limit(timeout=None):
52
+ """
53
+ A decorator for enforcing function execution time limits.
54
+
55
+ Args:
56
+ timeout (float or None): Maximum allowed execution time in seconds.
57
+ If None, no time limit is applied.
58
+
59
+ Returns:
60
+ The function's return value if completed in time.
61
+ Raises TimeoutError if execution exceeds the allowed time.
62
+ """
63
+ def decorator(func):
64
+ @wraps(func)
65
+ def wrapper(*args, **kwargs):
66
+ return run_with_timeout(func, args, kwargs, timeout)
67
+ return wrapper
68
+
69
+ return decorator
70
+
71
+
72
+ def remove_tag(s: str, tags: list[str] = ["<think>", "</think>", "<answer>", "</answer>"], r: str = "\n"):
73
+ for tag in tags:
74
+ s = s.replace(tag, r)
75
+ return s.strip(r).strip()
76
+
77
+
78
+ def parse_think_answer(text):
79
+ if not isinstance(text, str):
80
+ raise ValueError("Input text must be a string")
81
+
82
+ text = text.strip()
83
+
84
+ tag_think_start = "<think>"
85
+ tag_think_end = "</think>"
86
+ tag_answer_start = "<answer>"
87
+ tag_answer_end = "</answer>"
88
+
89
+ idx_answer_start = text.find(tag_answer_start)
90
+ idx_think_end = text.find(tag_think_end)
91
+
92
+ think_raw = ""
93
+ answer_raw = ""
94
+
95
+ if idx_answer_start != -1:
96
+ think_raw = text[:idx_answer_start]
97
+ answer_raw = text[idx_answer_start + len(tag_answer_start):]
98
+
99
+ idx_answer_end = answer_raw.find(tag_answer_end)
100
+ if idx_answer_end != -1:
101
+ answer_raw = answer_raw[:idx_answer_end]
102
+
103
+ elif idx_think_end != -1:
104
+ think_raw = text[:idx_think_end]
105
+ answer_raw = text[idx_think_end + len(tag_think_end):]
106
+
107
+ idx_answer_end = answer_raw.find(tag_answer_end)
108
+ if idx_answer_end != -1:
109
+ answer_raw = answer_raw[:idx_answer_end]
110
+
111
+ else:
112
+ raise ValueError("Could not parse response: missing <answer> or </think> tags.")
113
+
114
+ idx_think_start = think_raw.find(tag_think_start)
115
+ if idx_think_start != -1:
116
+ think_raw = think_raw[idx_think_start + len(tag_think_start):]
117
+
118
+ think = remove_tag(think_raw, [tag_think_start, tag_think_end, tag_answer_start, tag_answer_end], "")
119
+ answer = remove_tag(answer_raw, [tag_think_start, tag_think_end, tag_answer_start, tag_answer_end], "")
120
+
121
+ if not think:
122
+ raise ValueError("Parsed think content is empty")
123
+ if not answer:
124
+ raise ValueError("Parsed answer content is empty")
125
+
126
+ return think, answer
127
+
128
+
129
+ def extract_within_tags(content: str, start_tag='<answer>', end_tag='</answer>', default_return=None):
130
+ content = str(content)
131
+ start_index = content.rfind(start_tag)
132
+ if start_index != -1:
133
+ end_index = content.find(end_tag, start_index)
134
+ if end_index != -1:
135
+ return content[start_index + len(start_tag):end_index].strip()
136
+ return default_return
137
+
138
+
139
+ def get_all_file_paths(directory, suffix=''):
140
+ """
141
+ Get all file paths with the specified suffix in the directory.
142
+ """
143
+ file_paths = []
144
+ for root, dirs, files in os.walk(directory):
145
+ for file in files:
146
+ if file.endswith(suffix):
147
+ file_paths.append(os.path.join(root, file))
148
+ return file_paths
149
+
150
+
151
+ if __name__ == "__main__":
152
+ # python -m structai.utils
153
+ print("Testing utils.py...")
154
+ import time
155
+
156
+ # Test run_with_timeout
157
+ print("Testing run_with_timeout...")
158
+ def slow_func(seconds):
159
+ time.sleep(seconds)
160
+ return "done"
161
+
162
+ # Should pass
163
+ res = run_with_timeout(slow_func, args=(0.1,), timeout=1.0)
164
+ assert res == "done", f"[===ERROR===][structai][utils.py][main] run_with_timeout failed: {res} != done"
165
+
166
+ # Should fail
167
+ try:
168
+ run_with_timeout(slow_func, args=(0.5,), timeout=0.1)
169
+ print("run_with_timeout failed to timeout (unexpected)")
170
+ except TimeoutError:
171
+ print("run_with_timeout correctly timed out")
172
+
173
+ # Test timeout_limit decorator
174
+ print("Testing timeout_limit decorator...")
175
+ @timeout_limit(timeout=0.2)
176
+ def decorated_slow_func(seconds):
177
+ time.sleep(seconds)
178
+ return "done"
179
+
180
+ try:
181
+ decorated_slow_func(0.5)
182
+ print("timeout_limit failed to timeout (unexpected)")
183
+ except TimeoutError:
184
+ print("timeout_limit correctly timed out")
185
+
186
+ # Test remove_tag
187
+ print("Testing remove_tag...")
188
+ text = "<think>thinking...</think> <answer>42</answer>"
189
+ cleaned = remove_tag(text)
190
+ assert "thinking..." in cleaned and "42" in cleaned, f"[===ERROR===][structai][utils.py][main] remove_tag failed: {cleaned}"
191
+ assert "<think>" not in cleaned, f"[===ERROR===][structai][utils.py][main] remove_tag failed: {cleaned}"
192
+ print("remove_tag passed")
193
+
194
+ # Test parse_think_answer
195
+ print("Testing parse_think_answer...")
196
+ text = "<think>This is thinking</think><answer>This is answer</answer>"
197
+ think, answer = parse_think_answer(text)
198
+ assert think == "This is thinking", f"[===ERROR===][structai][utils.py][main] parse_think_answer failed: {think} != This is thinking"
199
+ assert answer == "This is answer", f"[===ERROR===][structai][utils.py][main] parse_think_answer failed: {answer} != This is answer"
200
+ print("parse_think_answer passed")
201
+
202
+ # Test extract_within_tags
203
+ print("Testing extract_within_tags...")
204
+ text = "prefix <tag>content</tag> suffix"
205
+ extracted = extract_within_tags(text, "<tag>", "</tag>")
206
+ assert extracted == "content", f"[===ERROR===][structai][utils.py][main] extract_within_tags failed: {extracted} != content"
207
+ print("extract_within_tags passed")
208
+
209
+ print("utils.py tests completed.")