py2wl 0.6.1__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.
- py2wl-0.6.1/LICENSE +21 -0
- py2wl-0.6.1/PKG-INFO +13 -0
- py2wl-0.6.1/README.md +1 -0
- py2wl-0.6.1/py2wl/__init__.py +13 -0
- py2wl-0.6.1/py2wl/compat/__init__.py +13 -0
- py2wl-0.6.1/py2wl/compat/_core/__init__.py +0 -0
- py2wl-0.6.1/py2wl/compat/_core/ai_plugin.py +133 -0
- py2wl-0.6.1/py2wl/compat/_core/ai_providers/__init__.py +0 -0
- py2wl-0.6.1/py2wl/compat/_core/ai_providers/base.py +30 -0
- py2wl-0.6.1/py2wl/compat/_core/ai_providers/claude.py +47 -0
- py2wl-0.6.1/py2wl/compat/_core/ai_providers/deepseek.py +46 -0
- py2wl-0.6.1/py2wl/compat/_core/ai_providers/gemini.py +46 -0
- py2wl-0.6.1/py2wl/compat/_core/ai_providers/groq.py +50 -0
- py2wl-0.6.1/py2wl/compat/_core/candidate_finder.py +154 -0
- py2wl-0.6.1/py2wl/compat/_core/cleaner.py +104 -0
- py2wl-0.6.1/py2wl/compat/_core/converters.py +465 -0
- py2wl-0.6.1/py2wl/compat/_core/error_classifier.py +107 -0
- py2wl-0.6.1/py2wl/compat/_core/fault_handler.py +186 -0
- py2wl-0.6.1/py2wl/compat/_core/interactor.py +134 -0
- py2wl-0.6.1/py2wl/compat/_core/kernel_ref.py +22 -0
- py2wl-0.6.1/py2wl/compat/_core/metadata.py +177 -0
- py2wl-0.6.1/py2wl/compat/_core/resolver.py +127 -0
- py2wl-0.6.1/py2wl/compat/_core/result_cache.py +163 -0
- py2wl-0.6.1/py2wl/compat/_proxy_base.py +288 -0
- py2wl-0.6.1/py2wl/compat/_state.py +9 -0
- py2wl-0.6.1/py2wl/compat/cProfile.py +3 -0
- py2wl-0.6.1/py2wl/compat/concurrent.py +3 -0
- py2wl-0.6.1/py2wl/compat/contextlib.py +3 -0
- py2wl-0.6.1/py2wl/compat/cupy.py +3 -0
- py2wl-0.6.1/py2wl/compat/datetime.py +3 -0
- py2wl-0.6.1/py2wl/compat/functools.py +3 -0
- py2wl-0.6.1/py2wl/compat/jax.py +3 -0
- py2wl-0.6.1/py2wl/compat/joblib.py +3 -0
- py2wl-0.6.1/py2wl/compat/line_profiler.py +3 -0
- py2wl-0.6.1/py2wl/compat/logging.py +3 -0
- py2wl-0.6.1/py2wl/compat/mappings/matplotlib.yaml +347 -0
- py2wl-0.6.1/py2wl/compat/mappings/monitoring.yaml +207 -0
- py2wl-0.6.1/py2wl/compat/mappings/numpy.yaml +1633 -0
- py2wl-0.6.1/py2wl/compat/mappings/numpy_extra.yaml +946 -0
- py2wl-0.6.1/py2wl/compat/mappings/pandas.yaml +1297 -0
- py2wl-0.6.1/py2wl/compat/mappings/perf.yaml +223 -0
- py2wl-0.6.1/py2wl/compat/mappings/scipy.yaml +1590 -0
- py2wl-0.6.1/py2wl/compat/mappings/sklearn.yaml +660 -0
- py2wl-0.6.1/py2wl/compat/mappings/sympy.yaml +1291 -0
- py2wl-0.6.1/py2wl/compat/mappings/tensorflow.yaml +586 -0
- py2wl-0.6.1/py2wl/compat/mappings/torch.yaml +724 -0
- py2wl-0.6.1/py2wl/compat/matplotlib.py +9 -0
- py2wl-0.6.1/py2wl/compat/memory_profiler.py +3 -0
- py2wl-0.6.1/py2wl/compat/monitoring.py +9 -0
- py2wl-0.6.1/py2wl/compat/mpl_toolkits.py +3 -0
- py2wl-0.6.1/py2wl/compat/multiprocessing.py +3 -0
- py2wl-0.6.1/py2wl/compat/numba.py +3 -0
- py2wl-0.6.1/py2wl/compat/numpy.py +11 -0
- py2wl-0.6.1/py2wl/compat/pandas.py +833 -0
- py2wl-0.6.1/py2wl/compat/perf.py +11 -0
- py2wl-0.6.1/py2wl/compat/psutil.py +3 -0
- py2wl-0.6.1/py2wl/compat/scipy.py +10 -0
- py2wl-0.6.1/py2wl/compat/seaborn.py +9 -0
- py2wl-0.6.1/py2wl/compat/sklearn.py +9 -0
- py2wl-0.6.1/py2wl/compat/sympy.py +10 -0
- py2wl-0.6.1/py2wl/compat/tensorflow.py +10 -0
- py2wl-0.6.1/py2wl/compat/time.py +3 -0
- py2wl-0.6.1/py2wl/compat/timeit.py +3 -0
- py2wl-0.6.1/py2wl/compat/torch.py +11 -0
- py2wl-0.6.1/py2wl/compat/tqdm.py +3 -0
- py2wl-0.6.1/py2wl/compat/warnings.py +3 -0
- py2wl-0.6.1/py2wl/kernel.py +333 -0
- py2wl-0.6.1/py2wl.egg-info/PKG-INFO +13 -0
- py2wl-0.6.1/py2wl.egg-info/SOURCES.txt +81 -0
- py2wl-0.6.1/py2wl.egg-info/dependency_links.txt +1 -0
- py2wl-0.6.1/py2wl.egg-info/requires.txt +8 -0
- py2wl-0.6.1/py2wl.egg-info/top_level.txt +1 -0
- py2wl-0.6.1/pyproject.toml +26 -0
- py2wl-0.6.1/setup.cfg +4 -0
- py2wl-0.6.1/tests/test_compat.py +302 -0
- py2wl-0.6.1/tests/test_fault.py +436 -0
- py2wl-0.6.1/tests/test_full.py +903 -0
- py2wl-0.6.1/tests/test_imitate.py +41 -0
- py2wl-0.6.1/tests/test_kernel_session.py +71 -0
- py2wl-0.6.1/tests/test_large_data.py +329 -0
- py2wl-0.6.1/tests/test_mappings_real.py +176 -0
- py2wl-0.6.1/tests/test_pandas.py +495 -0
- py2wl-0.6.1/tests/test_real_kernel.py +565 -0
py2wl-0.6.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2026.3.3] [TurinFohlen]
|
|
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.
|
py2wl-0.6.1/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py2wl
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: Python → Wolfram Language bridge via PTY, with compat layer for NumPy/SciPy/pandas/PyTorch etc.
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: pexpect
|
|
8
|
+
Requires-Dist: pyyaml
|
|
9
|
+
Provides-Extra: wxf
|
|
10
|
+
Requires-Dist: wolframclient>=1.4.0; extra == "wxf"
|
|
11
|
+
Provides-Extra: ai
|
|
12
|
+
Requires-Dist: requests; extra == "ai"
|
|
13
|
+
Dynamic: license-file
|
py2wl-0.6.1/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# py2wl - Python to Wolfram Bridge
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""py2wl 包(懒加载核心,避免在没有 pexpect 的环境爆炸)"""
|
|
2
|
+
|
|
3
|
+
def __getattr__(name):
|
|
4
|
+
if name == "WolframKernel":
|
|
5
|
+
from .kernel import WolframKernel
|
|
6
|
+
import sys
|
|
7
|
+
sys.modules[__name__].WolframKernel = WolframKernel
|
|
8
|
+
return WolframKernel
|
|
9
|
+
if name == "WolframPipeline":
|
|
10
|
+
# WolframPipeline 已在 WSTP 版本中移除,保留兼容名避免 ImportError
|
|
11
|
+
raise AttributeError(
|
|
12
|
+
"WolframPipeline 已移除,请直接使用 WolframKernel.evaluate()")
|
|
13
|
+
raise AttributeError(f"module 'py2wl' has no attribute {name!r}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
py2wl.compat
|
|
3
|
+
---------------------
|
|
4
|
+
NumPy/SciPy 兼容层入口。
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
from py2wl.compat import numpy as np
|
|
8
|
+
result = np.fft.fft([1, 2, 3, 4])
|
|
9
|
+
"""
|
|
10
|
+
from . import numpy
|
|
11
|
+
from ._core.converters import register_input_converter, register_output_converter
|
|
12
|
+
|
|
13
|
+
__all__ = ["numpy", "register_input_converter", "register_output_converter"]
|
|
File without changes
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
compat/_core/ai_plugin.py
|
|
3
|
+
--------------------------
|
|
4
|
+
AI 插件:当 YAML 映射表找不到规则时,由 AI 给出 Wolfram 函数名建议;
|
|
5
|
+
也用于容错系统的候选重排(candidate_finder.py)。
|
|
6
|
+
|
|
7
|
+
环境变量:
|
|
8
|
+
AI_PROVIDER = deepseek | claude | gemini | groq (默认 deepseek)
|
|
9
|
+
DEEPSEEK_API_KEY / ANTHROPIC_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY
|
|
10
|
+
WOLFRAM_AI_PLUGIN = 1 (必须设置才启用,避免意外网络调用)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import importlib
|
|
15
|
+
import pkgutil
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger("py2wl.compat")
|
|
20
|
+
|
|
21
|
+
_PROVIDERS_PKG = "py2wl.compat._core.ai_providers"
|
|
22
|
+
_HARDCODED_MAP = {
|
|
23
|
+
"deepseek": "DeepSeekProvider",
|
|
24
|
+
"claude": "ClaudeProvider",
|
|
25
|
+
"gemini": "GeminiProvider",
|
|
26
|
+
"groq": "GroqProvider",
|
|
27
|
+
}
|
|
28
|
+
_ENV_KEY_MAP = {
|
|
29
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
30
|
+
"claude": "ANTHROPIC_API_KEY",
|
|
31
|
+
"gemini": "GOOGLE_API_KEY",
|
|
32
|
+
"groq": "GROQ_API_KEY",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AIPlugin:
|
|
37
|
+
def __init__(self, api_key: str = None, provider_name: str = None):
|
|
38
|
+
self._name = (provider_name or os.getenv("AI_PROVIDER", "deepseek")).lower()
|
|
39
|
+
self._api_key = api_key
|
|
40
|
+
self._provider = None
|
|
41
|
+
|
|
42
|
+
def _ensure_provider(self) -> bool:
|
|
43
|
+
if self._provider is not None:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
pkg = importlib.import_module(_PROVIDERS_PKG)
|
|
47
|
+
dynamic = {
|
|
48
|
+
m: m.capitalize() + "Provider"
|
|
49
|
+
for _, m, _ in pkgutil.iter_modules(pkg.__path__)
|
|
50
|
+
if m not in ("__init__", "base")
|
|
51
|
+
}
|
|
52
|
+
full_map = {**dynamic, **_HARDCODED_MAP}
|
|
53
|
+
|
|
54
|
+
if self._name not in full_map:
|
|
55
|
+
log.warning(f"未知 AI 提供商:{self._name},可用:{list(full_map)}")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
if not self._api_key:
|
|
59
|
+
env = _ENV_KEY_MAP.get(self._name, self._name.upper() + "_API_KEY")
|
|
60
|
+
self._api_key = os.getenv(env)
|
|
61
|
+
if not self._api_key:
|
|
62
|
+
log.warning(f"未找到 {self._name} API Key,AI 插件已禁用")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
mod = importlib.import_module(f"{_PROVIDERS_PKG}.{self._name}")
|
|
67
|
+
self._provider = getattr(mod, full_map[self._name])(api_key=self._api_key)
|
|
68
|
+
log.info(f"AI 插件就绪:{self._name} / {self._provider.model}")
|
|
69
|
+
return True
|
|
70
|
+
except Exception as e:
|
|
71
|
+
log.warning(f"加载 AI 提供商失败:{e}")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
# ── 主功能 1:推断 Wolfram 函数名 ───────────────────────────
|
|
75
|
+
def suggest_mapping(self, python_path: str,
|
|
76
|
+
context: str = "") -> Optional[str]:
|
|
77
|
+
"""
|
|
78
|
+
给定 Python 函数路径,返回最可能对应的 Wolfram 函数名(仅函数名,无括号)。
|
|
79
|
+
context 可附加参数示例或错误信息辅助判断。
|
|
80
|
+
"""
|
|
81
|
+
if not self._ensure_provider():
|
|
82
|
+
return None
|
|
83
|
+
try:
|
|
84
|
+
prompt = (
|
|
85
|
+
f"你是 Wolfram Language 专家,熟悉 Python 科学计算生态(NumPy/SciPy/pandas/PyTorch 等)。\n"
|
|
86
|
+
f"用户调用了 Python 函数:{python_path}\n"
|
|
87
|
+
+ (f"上下文:{context}\n" if context else "")
|
|
88
|
+
+ f"\n只回答对应的 Wolfram Language 内置函数名(如 Fourier、LinearSolve),"
|
|
89
|
+
f"不要括号,不要参数,不要解释,不要换行。"
|
|
90
|
+
)
|
|
91
|
+
result = self._provider.generate(prompt, max_tokens=40, temperature=0.1)
|
|
92
|
+
# 取第一行,去掉可能残留的括号
|
|
93
|
+
return result.splitlines()[0].split("[")[0].strip()
|
|
94
|
+
except Exception as e:
|
|
95
|
+
log.warning(f"AI 函数名建议失败:{e}")
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
# ── 主功能 2:候选重排(供 candidate_finder 调用)───────────
|
|
99
|
+
def rerank(self, python_path: str, error_hint: str,
|
|
100
|
+
candidates: list) -> Optional[str]:
|
|
101
|
+
"""
|
|
102
|
+
给定错误上下文和候选列表,返回"最合适候选"的编号序列,如 "2,1,4"。
|
|
103
|
+
candidates: [(score, rule), ...]
|
|
104
|
+
"""
|
|
105
|
+
if not self._ensure_provider():
|
|
106
|
+
return None
|
|
107
|
+
try:
|
|
108
|
+
lines = "\n".join(
|
|
109
|
+
f" [{i+1}] {r['python_path']} → {r['wolfram_function']}"
|
|
110
|
+
f" ({r.get('description', '')})"
|
|
111
|
+
for i, (_, r) in enumerate(candidates)
|
|
112
|
+
)
|
|
113
|
+
prompt = (
|
|
114
|
+
f"用户在 py2wl 中调用了 `{python_path}` 但失败了。\n"
|
|
115
|
+
f"错误信息:{error_hint}\n\n"
|
|
116
|
+
f"以下是按编辑距离预筛的 Python→Wolfram 候选映射:\n{lines}\n\n"
|
|
117
|
+
f"请只输出编号序列(如 2,1,4),按'最可能是用户真正意图'排序,不要解释。"
|
|
118
|
+
)
|
|
119
|
+
return self._provider.generate(prompt, max_tokens=30, temperature=0.1)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
log.warning(f"AI 重排失败:{e}")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# ── 主功能 3:解释映射(供交互式 UI 调用)──────────────────
|
|
125
|
+
def explain(self, python_path: str, rule: dict) -> Optional[str]:
|
|
126
|
+
"""向用户解释一条 Python→Wolfram 映射的语义和注意事项。"""
|
|
127
|
+
if not self._ensure_provider():
|
|
128
|
+
return None
|
|
129
|
+
try:
|
|
130
|
+
return self._provider.explain_mapping(python_path, rule)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
log.warning(f"AI 映射解释失败:{e}")
|
|
133
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AIProvider(ABC):
|
|
6
|
+
"""
|
|
7
|
+
AI 服务提供商抽象基类。
|
|
8
|
+
py2wl 使用场景:
|
|
9
|
+
- suggest_mapping : 根据 Python 函数路径推断 Wolfram 等价函数名
|
|
10
|
+
- rerank_candidates: 对预筛候选列表按语义相关度重排
|
|
11
|
+
- explain_mapping : 向用户解释某条 Python→WL 映射的含义与注意事项
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
16
|
+
"""向模型发送原始 prompt,返回文本响应。"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def explain_mapping(self, python_path: str, rule: dict) -> str:
|
|
21
|
+
"""
|
|
22
|
+
解释一条 Python→Wolfram 映射规则。
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
python_path : 如 "numpy.linalg.eig"
|
|
26
|
+
rule : YAML 映射规则 dict,含 wolfram_function / description / tags 等
|
|
27
|
+
Returns:
|
|
28
|
+
面向开发者的说明文字(可含注意事项 / 坑)
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
from .base import AIProvider
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ClaudeProvider(AIProvider):
|
|
8
|
+
"""Anthropic Claude — py2wl AI 插件实现。"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, api_key: str = None):
|
|
11
|
+
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
12
|
+
if not self.api_key:
|
|
13
|
+
raise ValueError("需要提供 ANTHROPIC_API_KEY")
|
|
14
|
+
self.api_endpoint = "https://api.anthropic.com/v1/messages"
|
|
15
|
+
self.model = "claude-3-5-sonnet-20241022"
|
|
16
|
+
|
|
17
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
18
|
+
headers = {
|
|
19
|
+
"x-api-key": self.api_key,
|
|
20
|
+
"anthropic-version": "2023-06-01",
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
}
|
|
23
|
+
payload = {
|
|
24
|
+
"model": self.model,
|
|
25
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
26
|
+
"temperature": kwargs.get("temperature", 0.2),
|
|
27
|
+
"max_tokens": kwargs.get("max_tokens", 256),
|
|
28
|
+
}
|
|
29
|
+
resp = requests.post(self.api_endpoint, headers=headers,
|
|
30
|
+
json=payload, timeout=15)
|
|
31
|
+
resp.raise_for_status()
|
|
32
|
+
return resp.json()["content"][0]["text"].strip()
|
|
33
|
+
|
|
34
|
+
def explain_mapping(self, python_path: str, rule: dict) -> str:
|
|
35
|
+
wf = rule.get("wolfram_function", "?")
|
|
36
|
+
desc = rule.get("description", "")
|
|
37
|
+
tags = ", ".join(rule.get("tags") or [])
|
|
38
|
+
prompt = (
|
|
39
|
+
f"你是 Wolfram Language 和 Python 科学计算双栖专家。\n"
|
|
40
|
+
f"请用 2-4 句话解释以下映射关系,重点说明两侧语义差异和使用注意事项:\n\n"
|
|
41
|
+
f" Python : {python_path}\n"
|
|
42
|
+
f" Wolfram : {wf}\n"
|
|
43
|
+
f" 简介 : {desc}\n"
|
|
44
|
+
f" 标签 : {tags}\n\n"
|
|
45
|
+
f"如有数值精度差异、索引约定不同、广播行为差异等坑,请务必提醒。"
|
|
46
|
+
)
|
|
47
|
+
return self.generate(prompt, max_tokens=256, temperature=0.3)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
from .base import AIProvider
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DeepSeekProvider(AIProvider):
|
|
8
|
+
"""DeepSeek Chat — py2wl AI 插件实现。"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, api_key: str = None):
|
|
11
|
+
self.api_key = api_key or os.getenv("DEEPSEEK_API_KEY")
|
|
12
|
+
if not self.api_key:
|
|
13
|
+
raise ValueError("需要提供 DEEPSEEK_API_KEY")
|
|
14
|
+
self.api_endpoint = "https://api.deepseek.com/v1/chat/completions"
|
|
15
|
+
self.model = "deepseek-chat"
|
|
16
|
+
|
|
17
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
18
|
+
headers = {
|
|
19
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
}
|
|
22
|
+
payload = {
|
|
23
|
+
"model": self.model,
|
|
24
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
25
|
+
"temperature": kwargs.get("temperature", 0.2),
|
|
26
|
+
"max_tokens": kwargs.get("max_tokens", 256),
|
|
27
|
+
}
|
|
28
|
+
resp = requests.post(self.api_endpoint, headers=headers,
|
|
29
|
+
json=payload, timeout=15)
|
|
30
|
+
resp.raise_for_status()
|
|
31
|
+
return resp.json()["choices"][0]["message"]["content"].strip()
|
|
32
|
+
|
|
33
|
+
def explain_mapping(self, python_path: str, rule: dict) -> str:
|
|
34
|
+
wf = rule.get("wolfram_function", "?")
|
|
35
|
+
desc = rule.get("description", "")
|
|
36
|
+
tags = ", ".join(rule.get("tags") or [])
|
|
37
|
+
prompt = (
|
|
38
|
+
f"你是 Wolfram Language 和 Python 科学计算双栖专家。\n"
|
|
39
|
+
f"请用 2-4 句话解释以下映射关系,重点说明两侧语义差异和使用注意事项:\n\n"
|
|
40
|
+
f" Python : {python_path}\n"
|
|
41
|
+
f" Wolfram : {wf}\n"
|
|
42
|
+
f" 简介 : {desc}\n"
|
|
43
|
+
f" 标签 : {tags}\n\n"
|
|
44
|
+
f"如有数值精度差异、索引约定不同、广播行为差异等坑,请务必提醒。"
|
|
45
|
+
)
|
|
46
|
+
return self.generate(prompt, max_tokens=256, temperature=0.3)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
from .base import AIProvider
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GeminiProvider(AIProvider):
|
|
8
|
+
"""Google Gemini — py2wl AI 插件实现。"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, api_key: str = None):
|
|
11
|
+
self.api_key = api_key or os.getenv("GOOGLE_API_KEY")
|
|
12
|
+
if not self.api_key:
|
|
13
|
+
raise ValueError("需要提供 GOOGLE_API_KEY")
|
|
14
|
+
self.model = "gemini-1.5-flash"
|
|
15
|
+
self.api_endpoint = (
|
|
16
|
+
f"https://generativelanguage.googleapis.com/v1beta/"
|
|
17
|
+
f"models/{self.model}:generateContent"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
21
|
+
url = f"{self.api_endpoint}?key={self.api_key}"
|
|
22
|
+
payload = {
|
|
23
|
+
"contents": [{"parts": [{"text": prompt}]}],
|
|
24
|
+
"generationConfig": {
|
|
25
|
+
"temperature": kwargs.get("temperature", 0.2),
|
|
26
|
+
"maxOutputTokens": kwargs.get("max_tokens", 256),
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
resp = requests.post(url, json=payload, timeout=15)
|
|
30
|
+
resp.raise_for_status()
|
|
31
|
+
return resp.json()["candidates"][0]["content"]["parts"][0]["text"].strip()
|
|
32
|
+
|
|
33
|
+
def explain_mapping(self, python_path: str, rule: dict) -> str:
|
|
34
|
+
wf = rule.get("wolfram_function", "?")
|
|
35
|
+
desc = rule.get("description", "")
|
|
36
|
+
tags = ", ".join(rule.get("tags") or [])
|
|
37
|
+
prompt = (
|
|
38
|
+
f"你是 Wolfram Language 和 Python 科学计算双栖专家。\n"
|
|
39
|
+
f"请用 2-4 句话解释以下映射关系,重点说明两侧语义差异和使用注意事项:\n\n"
|
|
40
|
+
f" Python : {python_path}\n"
|
|
41
|
+
f" Wolfram : {wf}\n"
|
|
42
|
+
f" 简介 : {desc}\n"
|
|
43
|
+
f" 标签 : {tags}\n\n"
|
|
44
|
+
f"如有数值精度差异、索引约定不同、广播行为差异等坑,请务必提醒。"
|
|
45
|
+
)
|
|
46
|
+
return self.generate(prompt, max_tokens=256, temperature=0.3)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
from .base import AIProvider
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GroqProvider(AIProvider):
|
|
8
|
+
"""
|
|
9
|
+
Groq (Llama 3.3 70B) — py2wl AI 插件实现。
|
|
10
|
+
Groq 的推理延迟极低,适合容错系统的实时交互场景。
|
|
11
|
+
使用 REST API 而非 groq 库,保持零额外依赖。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, api_key: str = None):
|
|
15
|
+
self.api_key = api_key or os.getenv("GROQ_API_KEY")
|
|
16
|
+
if not self.api_key:
|
|
17
|
+
raise ValueError("需要提供 GROQ_API_KEY")
|
|
18
|
+
self.api_endpoint = "https://api.groq.com/openai/v1/chat/completions"
|
|
19
|
+
self.model = "llama-3.3-70b-versatile"
|
|
20
|
+
|
|
21
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
22
|
+
headers = {
|
|
23
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
}
|
|
26
|
+
payload = {
|
|
27
|
+
"model": self.model,
|
|
28
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
29
|
+
"temperature": kwargs.get("temperature", 0.2),
|
|
30
|
+
"max_tokens": kwargs.get("max_tokens", 256),
|
|
31
|
+
}
|
|
32
|
+
resp = requests.post(self.api_endpoint, headers=headers,
|
|
33
|
+
json=payload, timeout=15)
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
return resp.json()["choices"][0]["message"]["content"].strip()
|
|
36
|
+
|
|
37
|
+
def explain_mapping(self, python_path: str, rule: dict) -> str:
|
|
38
|
+
wf = rule.get("wolfram_function", "?")
|
|
39
|
+
desc = rule.get("description", "")
|
|
40
|
+
tags = ", ".join(rule.get("tags") or [])
|
|
41
|
+
prompt = (
|
|
42
|
+
f"你是 Wolfram Language 和 Python 科学计算双栖专家。\n"
|
|
43
|
+
f"请用 2-4 句话解释以下映射关系,重点说明两侧语义差异和使用注意事项:\n\n"
|
|
44
|
+
f" Python : {python_path}\n"
|
|
45
|
+
f" Wolfram : {wf}\n"
|
|
46
|
+
f" 简介 : {desc}\n"
|
|
47
|
+
f" 标签 : {tags}\n\n"
|
|
48
|
+
f"如有数值精度差异、索引约定不同、广播行为差异等坑,请务必提醒。"
|
|
49
|
+
)
|
|
50
|
+
return self.generate(prompt, max_tokens=256, temperature=0.3)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
compat/_core/candidate_finder.py
|
|
3
|
+
----------------------------------
|
|
4
|
+
候选函数查找器:在出错时,从元数据库 + AI 两路找出最相似的备选规则。
|
|
5
|
+
|
|
6
|
+
评分策略(综合得分 0-1):
|
|
7
|
+
1. 编辑距离(Levenshtein)相似度 ×0.4
|
|
8
|
+
2. 标签 / 关键词匹配数 ×0.4
|
|
9
|
+
3. AI 语义排名(可选) ×0.2
|
|
10
|
+
|
|
11
|
+
返回按得分降序排列的 [(score, rule), ...]。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
import logging
|
|
16
|
+
from typing import List, Tuple, Dict, Optional, TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .metadata import MetadataRepository
|
|
20
|
+
from .ai_plugin import AIPlugin
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger("py2wl.compat")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── 轻量 Levenshtein(零依赖)──────────────────────────────────
|
|
26
|
+
def _lev(a: str, b: str) -> int:
|
|
27
|
+
if len(a) < len(b):
|
|
28
|
+
a, b = b, a
|
|
29
|
+
prev = list(range(len(b) + 1))
|
|
30
|
+
for i, ca in enumerate(a, 1):
|
|
31
|
+
curr = [i]
|
|
32
|
+
for j, cb in enumerate(b, 1):
|
|
33
|
+
curr.append(min(prev[j] + 1, curr[j - 1] + 1,
|
|
34
|
+
prev[j - 1] + (ca != cb)))
|
|
35
|
+
prev = curr
|
|
36
|
+
return prev[-1]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _lev_sim(a: str, b: str) -> float:
|
|
40
|
+
max_len = max(len(a), len(b), 1)
|
|
41
|
+
return 1.0 - _lev(a, b) / max_len
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── 路径分词(对点分路径拆成词组进行匹配)──────────────────────
|
|
45
|
+
def _tokens(path: str) -> set:
|
|
46
|
+
"""numpy.fft.fft → {'numpy', 'fft'}"""
|
|
47
|
+
return set(path.replace("_", ".").split("."))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CandidateFinder:
|
|
51
|
+
"""
|
|
52
|
+
给定一个 python_path(可能拼错 / 不存在)和错误上下文,
|
|
53
|
+
返回按相关度排序的备选规则列表。
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, repo: "MetadataRepository",
|
|
57
|
+
ai_plugin: Optional["AIPlugin"] = None,
|
|
58
|
+
top_k: int = 6):
|
|
59
|
+
self._repo = repo
|
|
60
|
+
self._ai = ai_plugin
|
|
61
|
+
self._top_k = top_k
|
|
62
|
+
|
|
63
|
+
def find(self, python_path: str,
|
|
64
|
+
error_hint: str = "",
|
|
65
|
+
args: tuple = (),
|
|
66
|
+
kwargs: dict = None,
|
|
67
|
+
use_ai: bool = True) -> List[Tuple[float, Dict]]:
|
|
68
|
+
"""
|
|
69
|
+
返回 [(score, rule), ...],按 score 降序。
|
|
70
|
+
"""
|
|
71
|
+
kwargs = kwargs or {}
|
|
72
|
+
scored: Dict[str, Tuple[float, Dict]] = {}
|
|
73
|
+
|
|
74
|
+
target_tokens = _tokens(python_path)
|
|
75
|
+
target_ns = python_path.split(".")[0] # e.g. "numpy"
|
|
76
|
+
|
|
77
|
+
for rule in self._repo.all_rules:
|
|
78
|
+
path = rule["python_path"]
|
|
79
|
+
rule_ns = path.split(".")[0]
|
|
80
|
+
|
|
81
|
+
# ── 1. 编辑距离相似度 ────────────────────────────
|
|
82
|
+
lev_sc = _lev_sim(python_path, path)
|
|
83
|
+
|
|
84
|
+
# ── 2. 词集合 Jaccard 相似度 ──────────────────────
|
|
85
|
+
rule_tokens = _tokens(path)
|
|
86
|
+
union = target_tokens | rule_tokens
|
|
87
|
+
inter = target_tokens & rule_tokens
|
|
88
|
+
jac_sc = len(inter) / len(union) if union else 0.0
|
|
89
|
+
|
|
90
|
+
# ── 3. 命名空间加成(同一库优先)─────────────────
|
|
91
|
+
ns_bonus = 0.15 if rule_ns == target_ns else 0.0
|
|
92
|
+
|
|
93
|
+
# ── 4. 关键词 / 错误提示匹配 ─────────────────────
|
|
94
|
+
desc = (rule.get("description") or "").lower()
|
|
95
|
+
tags = " ".join(rule.get("tags") or []).lower()
|
|
96
|
+
hint_words = set(error_hint.lower().split())
|
|
97
|
+
hint_sc = sum(1 for w in hint_words if w in desc or w in tags) * 0.05
|
|
98
|
+
|
|
99
|
+
score = lev_sc * 0.45 + jac_sc * 0.35 + ns_bonus + min(hint_sc, 0.15)
|
|
100
|
+
if score > 0.05: # 过滤完全无关的
|
|
101
|
+
scored[path] = (score, rule)
|
|
102
|
+
|
|
103
|
+
# 按分数降序取 top_k
|
|
104
|
+
ranked = sorted(scored.values(), key=lambda x: -x[0])[: self._top_k]
|
|
105
|
+
|
|
106
|
+
# ── AI 二次排名(可选)───────────────────────────────
|
|
107
|
+
if use_ai and self._ai and ranked:
|
|
108
|
+
ranked = self._ai_rerank(python_path, error_hint, ranked)
|
|
109
|
+
|
|
110
|
+
return ranked
|
|
111
|
+
|
|
112
|
+
# ── AI 重排:让 AI 从 top_k 候选里选最合适的并排序 ──────
|
|
113
|
+
def _ai_rerank(self, python_path: str, error_hint: str,
|
|
114
|
+
candidates: List[Tuple[float, Dict]]) -> List[Tuple[float, Dict]]:
|
|
115
|
+
try:
|
|
116
|
+
candidate_lines = "\n".join(
|
|
117
|
+
f" [{i+1}] {r['python_path']} → {r['wolfram_function']} ({r.get('description','')})"
|
|
118
|
+
for i, (_, r) in enumerate(candidates)
|
|
119
|
+
)
|
|
120
|
+
prompt = (
|
|
121
|
+
f"用户调用了 Python 函数 `{python_path}` 但失败了({error_hint})。\n"
|
|
122
|
+
f"以下是按编辑距离预筛的候选映射:\n{candidate_lines}\n\n"
|
|
123
|
+
f"请只输出编号列表(如 2,1,4),按'最可能是用户真正意图'的顺序排列,不要解释。"
|
|
124
|
+
)
|
|
125
|
+
resp = self._ai._ensure_provider() and self._ai._provider.generate(
|
|
126
|
+
prompt, max_tokens=60)
|
|
127
|
+
if not resp or not isinstance(resp, str):
|
|
128
|
+
return candidates
|
|
129
|
+
|
|
130
|
+
# 解析 "2,1,4" 格式
|
|
131
|
+
import re
|
|
132
|
+
nums = [int(x) - 1 for x in re.findall(r"\d+", resp)
|
|
133
|
+
if 0 < int(x) <= len(candidates)]
|
|
134
|
+
if not nums:
|
|
135
|
+
return candidates
|
|
136
|
+
|
|
137
|
+
seen = set()
|
|
138
|
+
reordered = []
|
|
139
|
+
for n in nums:
|
|
140
|
+
if n not in seen:
|
|
141
|
+
seen.add(n)
|
|
142
|
+
# 稍微抬高 AI 排到前面的分数
|
|
143
|
+
sc, rule = candidates[n]
|
|
144
|
+
boost = (len(nums) - len(reordered)) * 0.03
|
|
145
|
+
reordered.append((min(sc + boost, 1.0), rule))
|
|
146
|
+
# 把未被 AI 提及的追加在后
|
|
147
|
+
for i, (sc, rule) in enumerate(candidates):
|
|
148
|
+
if i not in seen:
|
|
149
|
+
reordered.append((sc * 0.9, rule))
|
|
150
|
+
return reordered
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
log.debug(f"AI 重排失败(无影响):{e}")
|
|
154
|
+
return candidates
|