ChatLabel 0.0.1__tar.gz → 0.1.0__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.
- chatlabel-0.1.0/PKG-INFO +88 -0
- chatlabel-0.1.0/README.md +54 -0
- {chatlabel-0.0.1 → chatlabel-0.1.0}/pyproject.toml +16 -4
- chatlabel-0.1.0/src/ChatLabel.egg-info/PKG-INFO +88 -0
- {chatlabel-0.0.1 → chatlabel-0.1.0}/src/ChatLabel.egg-info/SOURCES.txt +11 -1
- chatlabel-0.1.0/src/ChatLabel.egg-info/requires.txt +19 -0
- {chatlabel-0.0.1 → chatlabel-0.1.0}/src/chatlabel/__init__.py +1 -1
- chatlabel-0.1.0/src/chatlabel/app.py +78 -0
- chatlabel-0.1.0/src/chatlabel/classifiers.py +183 -0
- chatlabel-0.1.0/src/chatlabel/cli.py +30 -0
- chatlabel-0.1.0/src/chatlabel/config.py +115 -0
- chatlabel-0.1.0/src/chatlabel/static/__init__.py +1 -0
- chatlabel-0.1.0/src/chatlabel/static/index.html +213 -0
- chatlabel-0.1.0/src/chatlabel/tasks.py +165 -0
- chatlabel-0.1.0/src/chatlabel/workbook.py +242 -0
- chatlabel-0.1.0/tests/test_classifiers.py +23 -0
- {chatlabel-0.0.1 → chatlabel-0.1.0}/tests/test_cli.py +13 -2
- chatlabel-0.1.0/tests/test_config.py +56 -0
- chatlabel-0.1.0/tests/test_tasks.py +36 -0
- {chatlabel-0.0.1 → chatlabel-0.1.0}/tests/test_version.py +1 -1
- chatlabel-0.1.0/tests/test_workbook.py +30 -0
- chatlabel-0.0.1/PKG-INFO +0 -94
- chatlabel-0.0.1/README.md +0 -65
- chatlabel-0.0.1/src/ChatLabel.egg-info/PKG-INFO +0 -94
- chatlabel-0.0.1/src/ChatLabel.egg-info/requires.txt +0 -14
- chatlabel-0.0.1/src/chatlabel/cli.py +0 -22
- chatlabel-0.0.1/src/chatlabel/config.py +0 -27
- {chatlabel-0.0.1 → chatlabel-0.1.0}/LICENSE +0 -0
- {chatlabel-0.0.1 → chatlabel-0.1.0}/setup.cfg +0 -0
- {chatlabel-0.0.1 → chatlabel-0.1.0}/src/ChatLabel.egg-info/dependency_links.txt +0 -0
- {chatlabel-0.0.1 → chatlabel-0.1.0}/src/ChatLabel.egg-info/entry_points.txt +0 -0
- {chatlabel-0.0.1 → chatlabel-0.1.0}/src/ChatLabel.egg-info/top_level.txt +0 -0
chatlabel-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ChatLabel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ChatLabel: structured annotation tasks with auditable workbook workflows
|
|
5
|
+
Author-email: ChatArch <1073853456@qq.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ChatArch/ChatLabel
|
|
8
|
+
Project-URL: Repository, https://github.com/ChatArch/ChatLabel
|
|
9
|
+
Project-URL: Documentation, https://arch.gh.wzhecnu.cn/ChatLabel/
|
|
10
|
+
Keywords: chatlabel,chatarch,cli
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: click<9.0,>=8.0
|
|
17
|
+
Requires-Dist: chatstyle<0.3.0,>=0.2.0
|
|
18
|
+
Requires-Dist: chatenv<0.3.0,>=0.2.10
|
|
19
|
+
Requires-Dist: fastapi<1.0,>=0.110
|
|
20
|
+
Requires-Dist: httpx<1.0,>=0.26
|
|
21
|
+
Requires-Dist: openpyxl<4.0,>=3.1
|
|
22
|
+
Requires-Dist: python-multipart<1.0,>=0.0.9
|
|
23
|
+
Requires-Dist: uvicorn<1.0,>=0.27
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest<10,>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: twine; extra == "dev"
|
|
28
|
+
Provides-Extra: docs
|
|
29
|
+
Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
|
|
30
|
+
Requires-Dist: mkdocs-material<10.0,>=9.5; extra == "docs"
|
|
31
|
+
Requires-Dist: mkdocs-static-i18n<2.0,>=1.2; extra == "docs"
|
|
32
|
+
Requires-Dist: mike<3.0,>=2.0; extra == "docs"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
<div align="center">
|
|
36
|
+
<a href="https://pypi.python.org/pypi/ChatLabel"><img src="https://img.shields.io/pypi/v/ChatLabel.svg" alt="PyPI 版本" /></a>
|
|
37
|
+
<a href="https://github.com/ChatArch/ChatLabel/actions/workflows/ci.yml"><img src="https://github.com/ChatArch/ChatLabel/actions/workflows/ci.yml/badge.svg" alt="测试状态" /></a>
|
|
38
|
+
<a href="https://arch.gh.wzhecnu.cn/ChatLabel/"><img src="https://img.shields.io/badge/docs-mkdocs-blue.svg" alt="项目文档" /></a>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div align="center">
|
|
42
|
+
|
|
43
|
+
[英文版](README.en.md) | [简体中文](README.md)
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
# ChatLabel
|
|
47
|
+
|
|
48
|
+
ChatLabel 是一个可审计的标注任务运行器。首个任务配置用于将印尼线路产品工作簿生成分类建议:上传 `.xlsx`、后台处理、查看进度、检查 WPS 单元格图片部件,并下载写入分类和备注的新工作簿。
|
|
49
|
+
|
|
50
|
+
> 自动输出是建议结果。完成图片逐项复核、高风险审计和一致性校验前,不应作为最终业务分类。
|
|
51
|
+
|
|
52
|
+
文档入口:<https://arch.gh.wzhecnu.cn/ChatLabel/>
|
|
53
|
+
|
|
54
|
+
## 安装与运行
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install ChatLabel
|
|
58
|
+
chatlabel --version
|
|
59
|
+
chatlabel --tree
|
|
60
|
+
chatlabel serve
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
默认页面:<http://127.0.0.1:8765/>
|
|
64
|
+
|
|
65
|
+
## 配置
|
|
66
|
+
|
|
67
|
+
ChatLabel 从 ChatEnv 读取名为 `apple` 的既有 OpenAI profile,也可通过环境变量调整:
|
|
68
|
+
|
|
69
|
+
| 变量 | 默认值 | 作用 |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `CHATLABEL_DATA_DIR` | `.chatlabel-data` | 上传文件与任务状态目录 |
|
|
72
|
+
| `CHATLABEL_CLASSIFIER` | `rules` | `rules` 或 `openai-responses` |
|
|
73
|
+
| `CHATLABEL_OPENAI_PROFILE` | `apple` | 要读取的 OpenAI profile |
|
|
74
|
+
|
|
75
|
+
进程级 `OPENAI_*` 会覆盖 profile 值。网页配置接口只显示模型、API 地址和“是否有凭据”,不会返回凭据内容。
|
|
76
|
+
|
|
77
|
+
## 开发验证
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python -m pip install -e ".[dev,docs]"
|
|
81
|
+
python -m pytest -q
|
|
82
|
+
chatlabel --help
|
|
83
|
+
chatlabel --tree
|
|
84
|
+
python -m build
|
|
85
|
+
mkdocs build --strict
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
进一步查看:[CLI 树](docs/cli-tree.md)、[能力地图](docs/capability-map.md)、[Python 接口树](docs/interface-tree.md)。
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://pypi.python.org/pypi/ChatLabel"><img src="https://img.shields.io/pypi/v/ChatLabel.svg" alt="PyPI 版本" /></a>
|
|
3
|
+
<a href="https://github.com/ChatArch/ChatLabel/actions/workflows/ci.yml"><img src="https://github.com/ChatArch/ChatLabel/actions/workflows/ci.yml/badge.svg" alt="测试状态" /></a>
|
|
4
|
+
<a href="https://arch.gh.wzhecnu.cn/ChatLabel/"><img src="https://img.shields.io/badge/docs-mkdocs-blue.svg" alt="项目文档" /></a>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
|
|
9
|
+
[英文版](README.en.md) | [简体中文](README.md)
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
# ChatLabel
|
|
13
|
+
|
|
14
|
+
ChatLabel 是一个可审计的标注任务运行器。首个任务配置用于将印尼线路产品工作簿生成分类建议:上传 `.xlsx`、后台处理、查看进度、检查 WPS 单元格图片部件,并下载写入分类和备注的新工作簿。
|
|
15
|
+
|
|
16
|
+
> 自动输出是建议结果。完成图片逐项复核、高风险审计和一致性校验前,不应作为最终业务分类。
|
|
17
|
+
|
|
18
|
+
文档入口:<https://arch.gh.wzhecnu.cn/ChatLabel/>
|
|
19
|
+
|
|
20
|
+
## 安装与运行
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install ChatLabel
|
|
24
|
+
chatlabel --version
|
|
25
|
+
chatlabel --tree
|
|
26
|
+
chatlabel serve
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
默认页面:<http://127.0.0.1:8765/>
|
|
30
|
+
|
|
31
|
+
## 配置
|
|
32
|
+
|
|
33
|
+
ChatLabel 从 ChatEnv 读取名为 `apple` 的既有 OpenAI profile,也可通过环境变量调整:
|
|
34
|
+
|
|
35
|
+
| 变量 | 默认值 | 作用 |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `CHATLABEL_DATA_DIR` | `.chatlabel-data` | 上传文件与任务状态目录 |
|
|
38
|
+
| `CHATLABEL_CLASSIFIER` | `rules` | `rules` 或 `openai-responses` |
|
|
39
|
+
| `CHATLABEL_OPENAI_PROFILE` | `apple` | 要读取的 OpenAI profile |
|
|
40
|
+
|
|
41
|
+
进程级 `OPENAI_*` 会覆盖 profile 值。网页配置接口只显示模型、API 地址和“是否有凭据”,不会返回凭据内容。
|
|
42
|
+
|
|
43
|
+
## 开发验证
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m pip install -e ".[dev,docs]"
|
|
47
|
+
python -m pytest -q
|
|
48
|
+
chatlabel --help
|
|
49
|
+
chatlabel --tree
|
|
50
|
+
python -m build
|
|
51
|
+
mkdocs build --strict
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
进一步查看:[CLI 树](docs/cli-tree.md)、[能力地图](docs/capability-map.md)、[Python 接口树](docs/interface-tree.md)。
|
|
@@ -5,11 +5,20 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ChatLabel"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "ChatLabel:
|
|
8
|
+
description = "ChatLabel: structured annotation tasks with auditable workbook workflows"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
|
12
|
-
dependencies = [
|
|
12
|
+
dependencies = [
|
|
13
|
+
"click>=8.0,<9.0",
|
|
14
|
+
"chatstyle>=0.2.0,<0.3.0",
|
|
15
|
+
"chatenv>=0.2.10,<0.3.0",
|
|
16
|
+
"fastapi>=0.110,<1.0",
|
|
17
|
+
"httpx>=0.26,<1.0",
|
|
18
|
+
"openpyxl>=3.1,<4.0",
|
|
19
|
+
"python-multipart>=0.0.9,<1.0",
|
|
20
|
+
"uvicorn>=0.27,<1.0",
|
|
21
|
+
]
|
|
13
22
|
authors = [{name = "ChatArch", email = "1073853456@qq.com"}]
|
|
14
23
|
keywords = ["chatlabel", "chatarch", "cli"]
|
|
15
24
|
classifiers = [
|
|
@@ -29,8 +38,8 @@ chatlabel = "chatlabel.cli:main"
|
|
|
29
38
|
chatlabel = "chatlabel.config"
|
|
30
39
|
|
|
31
40
|
[project.optional-dependencies]
|
|
32
|
-
dev = ["build", "pytest", "twine"]
|
|
33
|
-
docs = ["mkdocs>=1.6,<2.0", "mkdocs-material>=9.5,<
|
|
41
|
+
dev = ["build", "pytest>=8,<10", "twine"]
|
|
42
|
+
docs = ["mkdocs>=1.6,<2.0", "mkdocs-material>=9.5,<10.0", "mkdocs-static-i18n>=1.2,<2.0", "mike>=2.0,<3.0"]
|
|
34
43
|
|
|
35
44
|
[tool.setuptools.dynamic]
|
|
36
45
|
version = {attr = "chatlabel.__version__"}
|
|
@@ -40,3 +49,6 @@ where = ["src"]
|
|
|
40
49
|
|
|
41
50
|
[tool.setuptools]
|
|
42
51
|
include-package-data = true
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.package-data]
|
|
54
|
+
chatlabel = ["static/*"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ChatLabel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ChatLabel: structured annotation tasks with auditable workbook workflows
|
|
5
|
+
Author-email: ChatArch <1073853456@qq.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ChatArch/ChatLabel
|
|
8
|
+
Project-URL: Repository, https://github.com/ChatArch/ChatLabel
|
|
9
|
+
Project-URL: Documentation, https://arch.gh.wzhecnu.cn/ChatLabel/
|
|
10
|
+
Keywords: chatlabel,chatarch,cli
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: click<9.0,>=8.0
|
|
17
|
+
Requires-Dist: chatstyle<0.3.0,>=0.2.0
|
|
18
|
+
Requires-Dist: chatenv<0.3.0,>=0.2.10
|
|
19
|
+
Requires-Dist: fastapi<1.0,>=0.110
|
|
20
|
+
Requires-Dist: httpx<1.0,>=0.26
|
|
21
|
+
Requires-Dist: openpyxl<4.0,>=3.1
|
|
22
|
+
Requires-Dist: python-multipart<1.0,>=0.0.9
|
|
23
|
+
Requires-Dist: uvicorn<1.0,>=0.27
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest<10,>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: twine; extra == "dev"
|
|
28
|
+
Provides-Extra: docs
|
|
29
|
+
Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
|
|
30
|
+
Requires-Dist: mkdocs-material<10.0,>=9.5; extra == "docs"
|
|
31
|
+
Requires-Dist: mkdocs-static-i18n<2.0,>=1.2; extra == "docs"
|
|
32
|
+
Requires-Dist: mike<3.0,>=2.0; extra == "docs"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
<div align="center">
|
|
36
|
+
<a href="https://pypi.python.org/pypi/ChatLabel"><img src="https://img.shields.io/pypi/v/ChatLabel.svg" alt="PyPI 版本" /></a>
|
|
37
|
+
<a href="https://github.com/ChatArch/ChatLabel/actions/workflows/ci.yml"><img src="https://github.com/ChatArch/ChatLabel/actions/workflows/ci.yml/badge.svg" alt="测试状态" /></a>
|
|
38
|
+
<a href="https://arch.gh.wzhecnu.cn/ChatLabel/"><img src="https://img.shields.io/badge/docs-mkdocs-blue.svg" alt="项目文档" /></a>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div align="center">
|
|
42
|
+
|
|
43
|
+
[英文版](README.en.md) | [简体中文](README.md)
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
# ChatLabel
|
|
47
|
+
|
|
48
|
+
ChatLabel 是一个可审计的标注任务运行器。首个任务配置用于将印尼线路产品工作簿生成分类建议:上传 `.xlsx`、后台处理、查看进度、检查 WPS 单元格图片部件,并下载写入分类和备注的新工作簿。
|
|
49
|
+
|
|
50
|
+
> 自动输出是建议结果。完成图片逐项复核、高风险审计和一致性校验前,不应作为最终业务分类。
|
|
51
|
+
|
|
52
|
+
文档入口:<https://arch.gh.wzhecnu.cn/ChatLabel/>
|
|
53
|
+
|
|
54
|
+
## 安装与运行
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install ChatLabel
|
|
58
|
+
chatlabel --version
|
|
59
|
+
chatlabel --tree
|
|
60
|
+
chatlabel serve
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
默认页面:<http://127.0.0.1:8765/>
|
|
64
|
+
|
|
65
|
+
## 配置
|
|
66
|
+
|
|
67
|
+
ChatLabel 从 ChatEnv 读取名为 `apple` 的既有 OpenAI profile,也可通过环境变量调整:
|
|
68
|
+
|
|
69
|
+
| 变量 | 默认值 | 作用 |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `CHATLABEL_DATA_DIR` | `.chatlabel-data` | 上传文件与任务状态目录 |
|
|
72
|
+
| `CHATLABEL_CLASSIFIER` | `rules` | `rules` 或 `openai-responses` |
|
|
73
|
+
| `CHATLABEL_OPENAI_PROFILE` | `apple` | 要读取的 OpenAI profile |
|
|
74
|
+
|
|
75
|
+
进程级 `OPENAI_*` 会覆盖 profile 值。网页配置接口只显示模型、API 地址和“是否有凭据”,不会返回凭据内容。
|
|
76
|
+
|
|
77
|
+
## 开发验证
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python -m pip install -e ".[dev,docs]"
|
|
81
|
+
python -m pytest -q
|
|
82
|
+
chatlabel --help
|
|
83
|
+
chatlabel --tree
|
|
84
|
+
python -m build
|
|
85
|
+
mkdocs build --strict
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
进一步查看:[CLI 树](docs/cli-tree.md)、[能力地图](docs/capability-map.md)、[Python 接口树](docs/interface-tree.md)。
|
|
@@ -8,7 +8,17 @@ src/ChatLabel.egg-info/entry_points.txt
|
|
|
8
8
|
src/ChatLabel.egg-info/requires.txt
|
|
9
9
|
src/ChatLabel.egg-info/top_level.txt
|
|
10
10
|
src/chatlabel/__init__.py
|
|
11
|
+
src/chatlabel/app.py
|
|
12
|
+
src/chatlabel/classifiers.py
|
|
11
13
|
src/chatlabel/cli.py
|
|
12
14
|
src/chatlabel/config.py
|
|
15
|
+
src/chatlabel/tasks.py
|
|
16
|
+
src/chatlabel/workbook.py
|
|
17
|
+
src/chatlabel/static/__init__.py
|
|
18
|
+
src/chatlabel/static/index.html
|
|
19
|
+
tests/test_classifiers.py
|
|
13
20
|
tests/test_cli.py
|
|
14
|
-
tests/
|
|
21
|
+
tests/test_config.py
|
|
22
|
+
tests/test_tasks.py
|
|
23
|
+
tests/test_version.py
|
|
24
|
+
tests/test_workbook.py
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
click<9.0,>=8.0
|
|
2
|
+
chatstyle<0.3.0,>=0.2.0
|
|
3
|
+
chatenv<0.3.0,>=0.2.10
|
|
4
|
+
fastapi<1.0,>=0.110
|
|
5
|
+
httpx<1.0,>=0.26
|
|
6
|
+
openpyxl<4.0,>=3.1
|
|
7
|
+
python-multipart<1.0,>=0.0.9
|
|
8
|
+
uvicorn<1.0,>=0.27
|
|
9
|
+
|
|
10
|
+
[dev]
|
|
11
|
+
build
|
|
12
|
+
pytest<10,>=8
|
|
13
|
+
twine
|
|
14
|
+
|
|
15
|
+
[docs]
|
|
16
|
+
mkdocs<2.0,>=1.6
|
|
17
|
+
mkdocs-material<10.0,>=9.5
|
|
18
|
+
mkdocs-static-i18n<2.0,>=1.2
|
|
19
|
+
mike<3.0,>=2.0
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""FastAPI application for the local ChatLabel console."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from importlib import resources
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI, File, HTTPException, UploadFile
|
|
9
|
+
from fastapi.responses import FileResponse, HTMLResponse
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .config import AppConfig
|
|
13
|
+
from .tasks import TaskStore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@lru_cache(maxsize=1)
|
|
17
|
+
def get_config() -> AppConfig:
|
|
18
|
+
return AppConfig.from_env()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@lru_cache(maxsize=1)
|
|
22
|
+
def get_store() -> TaskStore:
|
|
23
|
+
return TaskStore(get_config())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_app() -> FastAPI:
|
|
27
|
+
"""Create the HTTP application without starting a server process."""
|
|
28
|
+
|
|
29
|
+
application = FastAPI(title="ChatLabel", version=__version__)
|
|
30
|
+
|
|
31
|
+
@application.get("/", response_class=HTMLResponse)
|
|
32
|
+
def index() -> str:
|
|
33
|
+
return resources.files("chatlabel.static").joinpath("index.html").read_text("utf-8")
|
|
34
|
+
|
|
35
|
+
@application.get("/api/config")
|
|
36
|
+
def config() -> dict[str, object]:
|
|
37
|
+
cfg = get_config()
|
|
38
|
+
return {
|
|
39
|
+
"classifier": cfg.classifier,
|
|
40
|
+
"data_dir": str(cfg.data_dir),
|
|
41
|
+
"openai": cfg.openai.public_dict(),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@application.get("/api/tasks")
|
|
45
|
+
def list_tasks() -> list[dict[str, object]]:
|
|
46
|
+
return [task.to_dict() for task in get_store().list_tasks()]
|
|
47
|
+
|
|
48
|
+
@application.post("/api/tasks")
|
|
49
|
+
async def create_task(file: UploadFile = File(...)) -> dict[str, object]:
|
|
50
|
+
try:
|
|
51
|
+
record = get_store().create_task(file.filename or "upload.xlsx", await file.read())
|
|
52
|
+
return record.to_dict()
|
|
53
|
+
except ValueError as exc:
|
|
54
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
55
|
+
|
|
56
|
+
@application.get("/api/tasks/{task_id}")
|
|
57
|
+
def get_task(task_id: str) -> dict[str, object]:
|
|
58
|
+
try:
|
|
59
|
+
return get_store().get_task(task_id).to_dict()
|
|
60
|
+
except FileNotFoundError as exc:
|
|
61
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
62
|
+
|
|
63
|
+
@application.get("/api/tasks/{task_id}/download")
|
|
64
|
+
def download_task(task_id: str) -> FileResponse:
|
|
65
|
+
try:
|
|
66
|
+
path = get_store().download_path(task_id)
|
|
67
|
+
except FileNotFoundError as exc:
|
|
68
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
69
|
+
return FileResponse(
|
|
70
|
+
path,
|
|
71
|
+
filename=path.name,
|
|
72
|
+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return application
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
app = create_app()
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Classifier boundaries for workbook rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .config import OpenAISettings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CATEGORIES = [
|
|
15
|
+
"普货",
|
|
16
|
+
"商检货",
|
|
17
|
+
"敏感类",
|
|
18
|
+
"A类特敏",
|
|
19
|
+
"B类特敏",
|
|
20
|
+
"纺织类",
|
|
21
|
+
"慢线半纺织",
|
|
22
|
+
"慢线纺织",
|
|
23
|
+
"不接",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ProductRow:
|
|
29
|
+
row_number: int
|
|
30
|
+
sku: str = ""
|
|
31
|
+
image_formula: str = ""
|
|
32
|
+
image_id: str = ""
|
|
33
|
+
name_cn: str = ""
|
|
34
|
+
name_en: str = ""
|
|
35
|
+
spec: str = ""
|
|
36
|
+
material: str = ""
|
|
37
|
+
existing_category: str = ""
|
|
38
|
+
existing_note: str = ""
|
|
39
|
+
|
|
40
|
+
def searchable_text(self) -> str:
|
|
41
|
+
parts = [self.name_cn, self.name_en, self.spec, self.material, self.existing_note]
|
|
42
|
+
return " ".join(str(part).lower() for part in parts if part)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ClassificationDecision:
|
|
47
|
+
category: str
|
|
48
|
+
note: str = ""
|
|
49
|
+
confidence: str = "medium"
|
|
50
|
+
source: str = "rules"
|
|
51
|
+
|
|
52
|
+
def __post_init__(self) -> None:
|
|
53
|
+
if self.category not in CATEGORIES:
|
|
54
|
+
raise ValueError(f"Unsupported category: {self.category}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Classifier(Protocol):
|
|
58
|
+
name: str
|
|
59
|
+
|
|
60
|
+
def classify(self, row: ProductRow) -> ClassificationDecision:
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class RuleBasedClassifier:
|
|
65
|
+
"""Generate a conservative first-pass proposal for the Indonesia profile."""
|
|
66
|
+
|
|
67
|
+
name = "rules"
|
|
68
|
+
|
|
69
|
+
def classify(self, row: ProductRow) -> ClassificationDecision:
|
|
70
|
+
text = row.searchable_text()
|
|
71
|
+
|
|
72
|
+
if self._contains(text, ["军用", "无人机", "drone", "仿真枪", "玩具枪", "迷彩", "压力罐", "白色粉末", "危险品"]):
|
|
73
|
+
return ClassificationDecision("不接", "命中明确不接规则", "high")
|
|
74
|
+
if self._contains(text, ["纯磁", "强磁", "磁铁"]) and not self._contains(text, ["弱磁", "磁吸", "带磁"]):
|
|
75
|
+
return ClassificationDecision("不接", "纯磁铁/强磁产品不接", "high")
|
|
76
|
+
if self._contains(text, ["胶水", "胶粘", "adhesive", "glue"]):
|
|
77
|
+
if self._contains(text, ["睫毛", "美甲", "化妆"]):
|
|
78
|
+
return ClassificationDecision("B类特敏", "需提供MSDS和运输鉴定报告,确认非危险品后可接", "high")
|
|
79
|
+
return ClassificationDecision("A类特敏", "需提供MSDS和运输鉴定报告,确认非危险品后可接", "high")
|
|
80
|
+
if self._contains(text, ["电池", "battery", "血氧仪", "血压仪", "助听器"]):
|
|
81
|
+
return ClassificationDecision("敏感类", "敏感品类,需复核运输资料", "high")
|
|
82
|
+
if self._contains(text, ["汽车配件", "摩托车", "汽配", "坐桶", "车厢内衬"]):
|
|
83
|
+
return ClassificationDecision("商检货", "汽配类按商检货处理", "high")
|
|
84
|
+
if self._contains(text, ["金属钥匙"]):
|
|
85
|
+
return ClassificationDecision("商检货", "金属钥匙按商检货处理", "high")
|
|
86
|
+
if self._contains(text, ["不锈钢", "五金", "铰链", "导轨", "置物架"]):
|
|
87
|
+
return ClassificationDecision("敏感类", "五金/不锈钢类按敏感类处理", "medium")
|
|
88
|
+
if self._contains(text, ["插电灯", "装饰灯", "灯串"]):
|
|
89
|
+
return ClassificationDecision("敏感类", "插电或装饰灯具按敏感类处理", "medium")
|
|
90
|
+
if self._contains(text, ["汽车灯", "手电筒", "太阳能灯"]):
|
|
91
|
+
return ClassificationDecision("普货", "灯具例外规则按普货处理", "high")
|
|
92
|
+
if self._contains(text, ["芳纶手套", "牛皮手套", "铅橡胶", "阻燃帽", "灭火毯", "防护铅衣", "舞蹈扇"]):
|
|
93
|
+
return ClassificationDecision("慢线半纺织", "含纺织/防护复合材质,按慢线半纺织初判", "medium")
|
|
94
|
+
if self._contains(text, ["乳贴", "胸贴"]):
|
|
95
|
+
return ClassificationDecision("B类特敏", "贴身内衣/护垫用途按B类特敏", "high")
|
|
96
|
+
if self._contains(text, ["穿戴甲", "甲片", "气泡柱", "腕带", "塑料手套", "丁腈手套", "tpe手套", "橡胶手套"]):
|
|
97
|
+
return ClassificationDecision("普货", "命中已确认普货样例/规则", "high")
|
|
98
|
+
if self._contains(text, ["钥匙扣"]) and self._contains(text, ["木", "榉木"]):
|
|
99
|
+
return ClassificationDecision("普货", "木质钥匙扣按普货,图片若含金属环需人工确认材质", "high")
|
|
100
|
+
if self._contains(text, ["磁吸", "带磁", "弱磁"]):
|
|
101
|
+
return ClassificationDecision("普货", "弱磁;主体分类仍需人工复核", "low")
|
|
102
|
+
|
|
103
|
+
if row.existing_category in CATEGORIES:
|
|
104
|
+
note = row.existing_note or "沿用源表已有分类,建议抽检复核"
|
|
105
|
+
return ClassificationDecision(row.existing_category, note, "low", "existing")
|
|
106
|
+
|
|
107
|
+
return ClassificationDecision("普货", "规则未明确命中,默认普货建议;必须人工复核", "low")
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _contains(text: str, needles: list[str]) -> bool:
|
|
111
|
+
return any(needle.lower() in text for needle in needles)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class OpenAIResponsesClassifier:
|
|
115
|
+
"""Responses API implementation behind the classifier protocol."""
|
|
116
|
+
|
|
117
|
+
name = "openai-responses"
|
|
118
|
+
|
|
119
|
+
def __init__(self, config: OpenAISettings, timeout: float = 60.0) -> None:
|
|
120
|
+
self.config = config
|
|
121
|
+
self.timeout = timeout
|
|
122
|
+
|
|
123
|
+
def classify(self, row: ProductRow) -> ClassificationDecision:
|
|
124
|
+
if not self.config.has_credentials:
|
|
125
|
+
raise RuntimeError("OpenAI credentials are not configured")
|
|
126
|
+
|
|
127
|
+
token = self.config.api_key or self.config.access_token
|
|
128
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
129
|
+
payload = {
|
|
130
|
+
"model": self.config.api_model,
|
|
131
|
+
"input": [
|
|
132
|
+
{
|
|
133
|
+
"role": "system",
|
|
134
|
+
"content": (
|
|
135
|
+
"你是印尼专线产品分类助手。只能输出JSON,category必须是九个合法分类之一,"
|
|
136
|
+
"note写必要资料要求或复核原因,confidence为high/medium/low。"
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
{"role": "user", "content": json.dumps(row.__dict__, ensure_ascii=False)},
|
|
140
|
+
],
|
|
141
|
+
"text": {
|
|
142
|
+
"format": {
|
|
143
|
+
"type": "json_schema",
|
|
144
|
+
"name": "classification_decision",
|
|
145
|
+
"strict": True,
|
|
146
|
+
"schema": {
|
|
147
|
+
"type": "object",
|
|
148
|
+
"additionalProperties": False,
|
|
149
|
+
"properties": {
|
|
150
|
+
"category": {"type": "string", "enum": CATEGORIES},
|
|
151
|
+
"note": {"type": "string"},
|
|
152
|
+
"confidence": {"type": "string", "enum": ["high", "medium", "low"]},
|
|
153
|
+
},
|
|
154
|
+
"required": ["category", "note", "confidence"],
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
161
|
+
response = client.post(f"{self.config.api_base}/responses", headers=headers, json=payload)
|
|
162
|
+
response.raise_for_status()
|
|
163
|
+
data = response.json()
|
|
164
|
+
|
|
165
|
+
parsed = json.loads(self._extract_output_text(data))
|
|
166
|
+
return ClassificationDecision(
|
|
167
|
+
category=parsed["category"],
|
|
168
|
+
note=parsed.get("note", ""),
|
|
169
|
+
confidence=parsed.get("confidence", "medium"),
|
|
170
|
+
source=self.name,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _extract_output_text(data: dict[str, object]) -> str:
|
|
175
|
+
if isinstance(data.get("output_text"), str):
|
|
176
|
+
return str(data["output_text"])
|
|
177
|
+
for item in data.get("output", []) if isinstance(data.get("output"), list) else []:
|
|
178
|
+
if not isinstance(item, dict):
|
|
179
|
+
continue
|
|
180
|
+
for content in item.get("content", []) if isinstance(item.get("content"), list) else []:
|
|
181
|
+
if isinstance(content, dict) and isinstance(content.get("text"), str):
|
|
182
|
+
return content["text"]
|
|
183
|
+
raise RuntimeError("Responses API result did not contain output text")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""CLI entrypoint for chatlabel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from chatstyle import add_tree_option
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
from chatlabel import __version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group(name="chatlabel", no_args_is_help=True)
|
|
13
|
+
@click.version_option(__version__, prog_name="chatlabel")
|
|
14
|
+
@add_tree_option(renderer_options={"root_name": "chatlabel"})
|
|
15
|
+
def main() -> None:
|
|
16
|
+
"""Run auditable annotation tasks from a local web console."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@main.command("serve")
|
|
20
|
+
@click.option("--host", default="127.0.0.1", show_default=True, help="Bind host.")
|
|
21
|
+
@click.option("--port", default=8765, show_default=True, type=click.IntRange(1, 65535), help="Bind port.")
|
|
22
|
+
@click.option("--reload", is_flag=True, help="Reload the server when source files change.")
|
|
23
|
+
def serve(host: str, port: int, reload: bool) -> None:
|
|
24
|
+
"""Start the local ChatLabel web console."""
|
|
25
|
+
|
|
26
|
+
uvicorn.run("chatlabel.app:app", host=host, port=port, reload=reload)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|