first commit

This commit is contained in:
ken 2025-03-30 09:13:46 +08:00
commit 6bd0544f45
140 changed files with 10303 additions and 0 deletions

131
.gitignore vendored Normal file
View File

@ -0,0 +1,131 @@
.git
code/*/__pycache__
code/*/*/__pycache__
code/*/*/*/__pycache__
code/*/.DS_Store
code/.DS_Store
code/*/*/.DS_Store
code/*/*/*/.DS_Store
.DS_Store
# Python
*.pyc
*.pyo
*.pyd
*.egg-info/
# Jupyter
.ipynb_checkpoints
# IDE
.vscode
.idea
*.wpr
*.wpu
.vs
x64
*.code-workspace
# Temp
build
dist
*.local
# VeighNa
.vntrader
# Visual Studio intermediate files
*.exp
*.iobj
*.ipdb
*.pdb
# Documents
_build
_static
_templates
# Misc
.DS_Store
*.mo
data
__pycache__
*.pth
data_synthesis
# See http://help.github.com/ignore-files/ for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile ~/.gitignore_global
/tmp
*/**/*un~
*/**/*.test
*un~
.DS_Store
*/**/.DS_Store
build/_vendor/pkg
/*.a
docs/readthedocs/build
/.VSCodeCounter
*.mo
#*
.#*
*#
*~
.project
.settings
# used by the Makefile
/build/_workspace/
/build/cache/
/build/bin/
.chat
# travis
profile.tmp
profile.cov
go.work*
docker-compose.*.yml
.env
coverage.out
__debug_bin
*.exe
*.log
*.hex
.tool-versions
salt.txt
*__debug_bin*
yarn.lock
node_modules
package-lock.json
*.pgo
/config.toml
/config.yaml
/config.yml
vendor
# my extension
*/build/
*/data/
*/**/*.dat
*/**/*.lck
.git
.github
*/**/prof.*
*/**/test.test
*.exe

145
.golangci.yml Normal file
View File

@ -0,0 +1,145 @@
run:
timeout: 10m
build-tags:
- nosqlite
- noboltdb
- integration
linters:
presets:
- bugs
- error
- unused
- performance
disable:
- gosec
- exhaustive
- musttag
- contextcheck
- wrapcheck
- err113
- unparam
- makezero #TODO: enable me
- noctx #TODO: enable me
- nilerr #TODO: enable me
- errorlint #TODO: enable me
- errchkjson #TODO: enable me
- unused #TODO: enable me
- testifylint #TODO: enable me
- gocheckcompilerdirectives
- protogetter
enable:
- unconvert
# - predeclared #TODO: enable me
# - thelper #TODO: enable me
- wastedassign
- gofmt
- gocritic
- perfsprint
# - revive
# - forcetypeassert
# - stylecheck
linters-settings:
gocritic: # see https://golangci-lint.run/usage/linters/#gocritic and https://go-critic.github.io/overview#checks-overview
enabled-tags:
- performance
- diagnostic
enabled-checks:
- ruleguard
disabled-checks:
- regexpMust
- appendAssign
- rangeValCopy
- exitAfterDefer
- elseif
- dupBranchBody
- assignOp
- singleCaseSwitch
- unlambda
- captLocal
- commentFormatting
- ifElseChain
# diagnostigs tag: possible bugs. need enable checks in future
- sloppyReassign
- sprintfQuotedString
- deferInLoop
- nilValReturn
- evalOrder
- returnAfterHttpError
- weakCond
- builtinShadowDecl
- unnecessaryDefer
- uncheckedInlineErr
- preferStringWriter
- commentedOutCode
- preferFprint
settings:
ruleguard:
rules: "rules.go"
hugeParam:
# size in bytes that makes the warning trigger (default 80)
sizeThreshold: 1000
rangeExprCopy:
# size in bytes that makes the warning trigger (default 512)
sizeThreshold: 512
# whether to check test functions (default true)
skipTestFuncs: true
underef:
# whether to skip (*x).method() calls where x is a pointer receiver (default true)
skipRecvDeref: true
govet:
disable:
- deepequalerrors
- fieldalignment
- shadow
- unsafeptr
goconst:
min-len: 2
min-occurrences: 2
gofmt:
simplify: false
issues:
exclude-rules:
- linters:
- golint
text: "should be"
- linters:
- errcheck
text: "not checked"
- linters:
- staticcheck
text: "SA(1019|1029|5011)"
# Exclude some linters from running on tests files.
- path: test\.go
linters:
- gosec
- unused
- gocritic
- perfsprint
- path: hack\.go
linters:
- gosec
- unused
- gocritic
- path: cmd/devp2p
linters:
- gosec
- unused
- gocritic
- path: metrics/sample\.go
linters:
- gosec
- gocritic
- path: p2p/simulations
linters:
- gosec
- gocritic
- path: p2p/dnsdisc
linters:
- gosec
- gocritic

47
Makefile Normal file
View File

@ -0,0 +1,47 @@
GO ?= go # if using docker, should not need to be installed/linked
GOBINREL = build/bin
GOBIN = $(CURDIR)/$(GOBINREL)
UNAME = $(shell uname) # Supported: Darwin, Linux
CGO_CFLAGS := $(shell $(GO) env CGO_CFLAGS 2>/dev/null) # don't lose default
# If it is arm64 or aarch64, then we need to use portable version of blst. use or with stringw "arm64" and "aarch64" to support both
ifeq ($(shell uname -m), arm64)
CGO_CFLAGS += -D__BLST_PORTABLE__
endif
ifeq ($(shell uname -m), aarch64)
CGO_CFLAGS += -D__BLST_PORTABLE__
endif
CGO_CFLAGS += -Wno-unknown-warning-option -Wno-enum-int-mismatch -Wno-strict-prototypes -Wno-unused-but-set-variable -O3
CGO_LDFLAGS := $(shell $(GO) env CGO_LDFLAGS 2> /dev/null)
CGO_LDFLAGS += -O3 -g
ifeq ($(shell uname -s), Darwin)
ifeq ($(filter-out 13.%,$(shell sw_vers --productVersion)),)
CGO_LDFLAGS += -mmacosx-version-min=13.3
endif
endif
GO_FLAGS += -trimpath -tags $(BUILD_TAGS) -buildvcs=false
GO_FLAGS += -ldflags '-extldflags "-Wl,--allow-multiple-definition"'
GOBUILD = CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" GOPRIVATE="$(GOPRIVATE)" $(GO) build $(GO_FLAGS)
default: all
## go-version: print and verify go version
go-version:
@if [ $(shell $(GO) version | cut -c 16-17) -lt 20 ]; then \
echo "minimum required Golang version is 1.20"; \
exit 1 ;\
fi
agent:
$(GOBUILD) -o $(GOBIN)/agent ./src/main.go
# build each command using %.cmd rule
$(COMMANDS): %: %.cmd
## all:
all: agent $(COMMANDS)

278
README.md Normal file
View File

@ -0,0 +1,278 @@
#### 1. AI agent
Agent模块负责处理request工作流生成prompt与LLM交互。
Agent 有三模式运行由plugin在request的model参数决定运行模式在交互过程对agent是透明的。
> 1. agent local model就是agent调用本地部署的LLM模型
> 2. agent remote api就是agent调用openai或deepseek等外部服务的api
> 3. plugin remote api由plugin调用openai或deepseek等外部服务的api
agent网页版测试 <https://host:port/simpletest2025>
前端测试代码 repo/examples
测试api_key: “***simpletest2025***_xxxx” xxxx为任意字符串
##### 1.1 系统框架
<img src="assets/架构.png" alt="drawing" style="width:700px;"/>
ideservice 是须要plugin提供给agent的一些获取IDE信息的基础功能为穿透NAT需要websocket连接。
##### 1.2 workflow
```mermaid
sequenceDiagram
plugin->>agent: request
agent-->>ideservice: more info
ideservice-->>agent: file, symbol
agent->>plugin: result
```
agent有可能会多次向ideservice询问更多的信息这对于plugin是透明的。
##### 1.3 Task Pool
章节2中所有带cmd参数带前缀“exec”的request都是涉及LLM模型调用的。
Agent用一个FIFO队列缓存所有用户的未处理任务当队列长度过大时agent会拒绝新来的任务。
同一APIkey同时能建立5个链接一个连接只能有一个任务在队列同时提交两个task, 前一个会被取消。
#### 2. Agent协议
plugin通过websocket连接agent。如果使用https, 需要设置client接受agent的https认证证书。
##### 2.1 身份认证
建立连接时带header "X-Api-Key" : api_key如果api_key不对agent主动断开连接
```javascript
let client = new ws.WebSocket('wss://host:8080/ws', {headers: {
["X-Api-Key"]: "simpletest2025_001",
}})
```
##### 2.2 获取Agent支持的模型 list_model
```javascript
request:
{
"request_id": 123, // 随机生成 (必填)
"cmd" : "list_model"
}
response:
{
"request_id": 123, // 同request
"models" : ["local deepseek-R1:32b", "deepseek-R1:32b"] // (必填)
}
```
##### 2.3 生成代码解释 (exec_explain)
```javascript
request:
{
"cmd" : "exec_explain",
"request_id" : 123, // 随机生成 (必填)
"language" : "en", // 语言, en-英文, zh-中文,默认为 en (可选)
"selected_text": text_context, // 选取的代码片段 (必填)
"visible_text" : text_context, // ide窗口可见的代码片段 (必填)
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false // stream方式返回可选
}
text_context:
{
"filepath": "example\\\\game.py", // 文件路径
"range": {
"start": { "line": 33, "character": 0}, // 从33行0字符开始
"end" : { "line": 45, "character": 41} // 到45行41字符结束
},
"text": "..." // 源码片段
}
成功返回:
{
"request_id" : 123, // 同request必填
"msg" : "", // 返回内容
"stream_seq_id": 0, // stream=true时才有, 从0开始可选
"stream_finsh" : false, // stream=true时才有, true-stream结束可选
}
失败返回:
{
"request_id": 123, // 同request必填
"error" : "cancelled" // 出错信息,没有就是调用成功(必填)
}
```
##### 2.4 生成代码文档 (exec_docstring)
目前只支持python
```javascript
request:
{
"cmd" : "exec_docstring",
"request_id" : 123, // 随机生成 (必填)
"language" : "en", // 语言, en-英文, zh-中文,默认为 en (可选)
"selected_text": text_context, // 选取的代码片段 (必填)
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false // stream方式返回 (可选)
}
返回json格式同 2.3
```
##### 2.5 代码优化 (exec_optimize)
//TODO 未测试效果, prompt该如何描述优化
```javascript
request:
{
"cmd" : "exec_optimize",
"request_id" : 123, // 随机生成 (必填)
"language" : "en", // 语言, en-英文, zh-中文,默认为 en (可选)
"selected_text": "", // 选取的代码片段 (必填)
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false // stream方式返回 (可选)
}
返回json格式同 2.3
```
##### 2.6 生成单元测试代码 (exec_fix)
```javascript
request:
{
"cmd" : "exec_fix",
"request_id" : 123, // 随机生成 (必填)
"language" : "en", // 语言, en-英文, zh-中文,默认为 en (可选)
"selected_text": "", // 选取的代码片段 (必填)
"model" : "local deepseek-r1:32b",//(必填)
"stream" : false // stream方式返回 (可选)
}
返回json格式同 2.3
```
##### 2.7 生成单元测试代码 (exec_unittest)
// TODO 与ideservice交互
目前仅支持英文回复, 流程对devchat做了简化
```mermaid
sequenceDiagram
plugin->>agent: exec_unittest_recommend 请求测试建议
agent->>plugin: 测试建议
plugin->>agent: exec_unittest_code 请求测试代码
plugin->>agent: 测试代码
```
```javascript
请求测试建议:
{
"cmd" : "exec_unittest_recommend",
"request_id": 123, // 随机生成 (必填)
"language" : "en", // 语言, en-英文, zh-中文,默认为 en (可选)
"user_prompt" : "", // 用户定制化描述 (可选)
"file_content" : "..." // 整个文件内容(必填)
"filepath" : "\\example\\game.py", // 文件路径(必填)
"func_name" : "next_turn", // 函数名 (必填)
"func_location": { // 函数位置(必填)
"start": { "line": 33, "character": 0},
"end" : { "line": 45, "character": 41},
},
"func_symbols": [ // function 内的所以变量名和函数名 (必填)
{
"name": "next_turn",
"kind": "Function",
"range": { // 位置
"start": { "line": 33, "character": 0},
"end" : { "line": 45, "character": 41}
},
"children": [
{
"name": "WIDTH",
"kind": "Constant",
"range": { // 位置
"start": { "line": 33, "character": 0},
"end" : { "line": 45, "character": 41}
},
"children": [
...
]
},
]
},
...
],
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false // stream方式返回 (可选)
}
返回json格式同 2.3
请求测试代码:
{
"cmd" : "exec_unittest_code",
"request_id" : 123, // 随机生成 (必填)
"language" : "en", // 语言, en-英文, zh-中文,默认为 en (可选)
"test_cases_str": "", // exec_unittest_recommend 提供的 (必填)
// 如果task_id为空须填写
"function_name" : "" // 函数名 (可选)
"user_prompt" : "" // 定制化描述 (可选)
"file_path" : "./src/test.py" // 文件在项目里的相对路径 (可选)
"relevant_content" : "" // the relevant source code of the function (可选)
"reference_content": "" // the relevant source code of the function (可选)
"stream": false // stream方式返回可选
}
返回json格式同 2.3
```
##### 2.8 和大模型对话 (exec_chat)
```javascript
request:
{
"cmd" : "exec_chat",
"request_id": 123, // 随机生成 (必填)
"msg" : "", // 内容 (必填)
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false // stream方式返回可选
}
返回json格式同 2.3
```
#### 3. ide_service协议
开发优先级低解决2.7“与ideservice交互”问题前未用到
是由 agent 向 ide_service 请求,参考
<https://github.com/devchat-ai/devchat-vscode/blob/main/src/ide_services/services.ts>
只保留以下服务,
```javascript
"/get_document_symbols": {
keys: ["abspath"],
handler: getDocumentSymbols,
},
"/find_def_locations": {
keys: ["abspath", "line", "character"],
handler: findDefinitionLocations,
},
"/find_type_def_locations": {
keys: ["abspath", "line", "character"],
handler: findTypeDefinitionLocations,
},
```
#### 3. 功能覆盖
<img src="assets/功能覆盖.png"/>

File diff suppressed because one or more lines are too long

111
assets/home.html Normal file
View File

@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chat Example</title>
<script type="text/javascript">
window.onload = function () {
var conn;
var msg = document.getElementById("msg");
var log = document.getElementById("log");
function appendLog(item) {
var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
log.appendChild(item);
if (doScroll) {
log.scrollTop = log.scrollHeight - log.clientHeight;
}
}
document.getElementById("msg").value = ""
document.getElementById("form").onsubmit = function () {
if (!conn) {
return false;
}
if (!msg.value) {
return false;
}
conn.send(msg.value);
msg.value = "";
return false;
};
if (window["WebSocket"]) {
conn = new WebSocket("wss://" + document.location.host+"/ws");
conn.onclose = function (evt) {
var item = document.createElement("div");
item.innerHTML = "<b>Connection closed.</b>";
appendLog(item);
};
conn.onmessage = function (evt) {
var messages = evt.data.split('\n');
for (var i = 0; i < messages.length; i++) {
var item = document.createElement("div");
item.innerText = messages[i];
appendLog(item);
}
};
} else {
var item = document.createElement("div");
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
appendLog(item);
}
};
</script>
<style type="text/css">
html {
overflow: hidden;
}
body {
overflow: hidden;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
background: gray;
}
#log {
background: white;
margin: 1em;
padding: 0.5em 0.5em 0.5em 0.5em;
position: absolute;
top: 0.5em;
left: 0.5em;
right: 0.5em;
bottom: 50%;
height: 50%;
overflow: auto;
}
#form {
padding: 0.5em 0.5em 0.5em 0.5em;
position: absolute;
top: 55%;
bottom: 1em;
left: 0px;
width: 100%;
height: 60%;
overflow: hidden;
}
#msg {
padding: 0.5em 0.5em 0.5em 0.5em;
position: absolute;
top: 5%;
width: 80%;
height: 50%;
}
</style>
</head>
<body>
<div id="log"></div>
<form id="form">
<input type="submit" value="Send" />
<textarea type="text" id="msg" size="100%" aria-valuetext="" autofocus />
</form>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
assets/katex/katex.min.css vendored Normal file

File diff suppressed because one or more lines are too long

2029
assets/mermaid/mermaid.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
assets/功能覆盖.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

BIN
assets/架构.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
assets/架构.pptx Normal file

Binary file not shown.

BIN
assets/项目计划.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

17
config/server.crt Normal file
View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICrzCCAZcCFC7ZJvBbY2kSpHmNcVanfTX1GlrvMA0GCSqGSIb3DQEBCwUAMBQx
EjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNTAzMjExNjQzMThaFw0yNjAzMjExNjQz
MThaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAPs58pQXpUzqBQR+ETZGK8qKBPNKvNeBm0DEQehXd9z4wsu9iTG9
OHXI8Gz7QRcPrh6iJhmO4ETzm/N73Bu2ZxtAMmvegXxFRkHLJFy55Tz2wJQtgogY
qwQGBQ3PEeh6e93xh5GlOyab9eQaL7F76Q21u9U3/kOUi8iqe09nqMdT+DCc9IFG
nwmE0P314V2URlNKhgoxW7iWrVhFD7a8c6gsJrKCeCVQxoByqwqTS/nsgjul/9TU
2Gu196tQCj4laR3fxPX0Ivnh2PLxHymxih2RPywHmPhF44S/q5FtVogtRr6/nEJL
f75wgpcxZX5SDTFZt2V/eFkc1LdiV1I3f8UCAwEAATANBgkqhkiG9w0BAQsFAAOC
AQEADvJPc9fKHQktSMBwVgoQWUh26iZ1Cact9dIj0PZcZlfG/d8UB9zLW7CIDQPX
/X1EgyahJPSIK80xeI7LLumYuGnV+r1yPT7o/6IV8csmJAcMCaiUhrjhaixIIsMb
UuelXNXHkCWzIxJbr20pPiK67CEqwozD8Bu+HcforYuVfzs3lIMHSXHiRwxo1M88
2u4LclpwormdxpHHIw0aWqh3B3A6+qZkz2vbQ631Ol29h9Lzezh3+FtH1HXJfpyr
flJ6LkLP2U4u7rq5iAUmj8QHjqmHyhpokwESEnPxrFWYjxD0UJlXWbGrfxEkwRiL
dyBoou22FYBXrPfMeZ7Ntbeu6g==
-----END CERTIFICATE-----

15
config/server.csr Normal file
View File

@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEA+znylBelTOoFBH4RNkYryooE80q814GbQMRB6Fd3
3PjCy72JMb04dcjwbPtBFw+uHqImGY7gRPOb83vcG7ZnG0Aya96BfEVGQcskXLnl
PPbAlC2CiBirBAYFDc8R6Hp73fGHkaU7Jpv15BovsXvpDbW71Tf+Q5SLyKp7T2eo
x1P4MJz0gUafCYTQ/fXhXZRGU0qGCjFbuJatWEUPtrxzqCwmsoJ4JVDGgHKrCpNL
+eyCO6X/1NTYa7X3q1AKPiVpHd/E9fQi+eHY8vEfKbGKHZE/LAeY+EXjhL+rkW1W
iC1Gvr+cQkt/vnCClzFlflINMVm3ZX94WRzUt2JXUjd/xQIDAQABoAAwDQYJKoZI
hvcNAQELBQADggEBAKnjUXH2pjIKHeM1tvTjUQ+jpJmsblRvciPDZrOtPHaDlarW
SmGx8VeOADOMhe6soPqDWZKjFqFgPznklRw5KRDuQmp2UWAwC4aHRLBkBtA69xCE
iZPlu5QgxvEYvBp5+J4t1bdg9UN/98sCQda4uN1gajy4kt4uM51eXoQ41HOulO59
CcZOBPrGr6cawpG8h9te5PRGtRvAQDpI52mq47ZI4Uo2MiOJAGPLh66inLZqQMYO
rHKFdrHFjhD0MkI2U4aarBqBRQVV/dJPsDCzx73IQhng9AIS6RaYnOZvRgbuXeuT
hGEd95VPVqZR+r9OX+/qXFq1FfDdaD7Cl1uoBdQ=
-----END CERTIFICATE REQUEST-----

27
config/server.key Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA+znylBelTOoFBH4RNkYryooE80q814GbQMRB6Fd33PjCy72J
Mb04dcjwbPtBFw+uHqImGY7gRPOb83vcG7ZnG0Aya96BfEVGQcskXLnlPPbAlC2C
iBirBAYFDc8R6Hp73fGHkaU7Jpv15BovsXvpDbW71Tf+Q5SLyKp7T2eox1P4MJz0
gUafCYTQ/fXhXZRGU0qGCjFbuJatWEUPtrxzqCwmsoJ4JVDGgHKrCpNL+eyCO6X/
1NTYa7X3q1AKPiVpHd/E9fQi+eHY8vEfKbGKHZE/LAeY+EXjhL+rkW1WiC1Gvr+c
Qkt/vnCClzFlflINMVm3ZX94WRzUt2JXUjd/xQIDAQABAoIBAQCUz4plNmxv1t8J
/jLr1zzxSDgOdOLPNLJl9tqjacxSU/fu/2JY2CCthE0GAP5ZTqxw8NNoqD0oGrTx
0RhcQftpJOm5A5/noJExP8dWNbcYGYGRRDPhsdr7EyXDPDj7YDInEX6gQyN6hz9r
0icWFrPBrziH4Bb4qPDz+Cf6rQhIqMQVVSt3SxzTeXik3SdNLBOuPYea2cOXPcIH
+9LmqFp2v+qmPs/DmLUR8qr1qEtL2aZ42FM0wlk+oyiA4h9YGAxHhXTL93QRtpsm
JCLizdE4jk/4MJrOFbeWHYsBnuJo4Ohr0G+giIaij+JxoWCv8wjuJ3glxRXRM/Mm
fp9WwOExAoGBAP7NO6NDtSO9rNQ7BivTU/XvQOdUIeWtxBBbO+Eb72ElGjoavQjZ
6wkgR40CBaJArb12f3//T2YiKHn7abRAaJSsGZcZYbFemrt8BRoh940arlmZT2U3
h4O6JTGSQ0AqsSLiX/f1+mq0BsYhPXnMlL8o1lOVjrTDjxPdb49WhHNzAoGBAPxo
aPz1kLdDXHUpj9mOqRUakQHCNjHLnJvXuZSDdhice5y7X0JHCrsQMQviEpODIylp
pwouKwJHd1bHcCkHNh05f4KNb5kWQvEQr1fWlFH1rIcmMi5u44etIa7+96AxTlbK
05PU7Z2bPSaEb4NwZxGek3Bi8BS1U+dY684liKHnAoGBAOxndG6tEjIf+GtMfBRL
VTn4CmrWp0CW8TMPwsXACmBCcUv5pOvCbAwik+AjQSDiC5Cekfsq4PrstEtA1Tj1
DVLrPi7v0LElCkDq9zzgyPJ1D9B3qq9JTwULvxh4EMdHFOQ9zjacVQONFj/bdVM9
WcDmXbxdJXNXgbc3mxsNdYQ7AoGAVPMrCp/3UcapEoGneXhLLeAgF7BunvnBPq4v
Nm28THI2JDznxZ21XdoIN9ChoeYL6n81ifaZ4Q1+e8Vp2VUlAFb+tEi325UjeJC+
AtkkWCVQKLhRQVFu4JYDdSs42kBd9lYTOXj7dATwYstF4L3u4n41byjNVprkn0NN
MbduMdcCgYEA0mdWgBMRf6GBbhWHM0GOnEd7prRqQcc/v/UpYV1QvLvRpxgFmX0o
Nff/HFndeILEltANMyKNCmenHPQmCJbX2by/LQ4L8cNp36NiJZSHhlG6NVjn3+HD
DRq3I2idyd3IBS5yEyG7+1vMRXJLHE27g4934IAqZ+9ccJK8nR6O/7A=
-----END RSA PRIVATE KEY-----

749
docs/分析结果.md Normal file
View File

@ -0,0 +1,749 @@
## 竞品分析与效果对比
### 1 思码逸devchat
> 官网 <https://www.devchat.ai/zh>
> 源码
> workflow <https://github.com/devchat-ai/workflows>
> services <https://github.com/devchat-ai/devchat>
> vscode extension <https://github.com/devchat-ai/devchat-vscode>
devchat有两种处理逻辑
1. 一次用prompt + code 的方式询问大模型LLM
2. 多次询问LLM第一次LLM会返回缺少的信息在下一次询问时补充告诉LLM见/unit_test命令
#### 1.1 生成代码块解释 `/explain`
用的prompt是
```python
"""
Your task is:
Explain the code.
Following the task requirements, explain the selected portion of the code. \
Note that you only explain the logic of the selected code. \
The visible code only serves as the context for you to understand the code. \
Here is the relevant context \
information for your reference:
1. selected code info: {selected_text}
2. current visible code info: {visible_text}
"""
```
selected_text 是编辑器中被选中的代码
visible_text 是当前编辑器中可见代码
#### 1.2 生成单元测试代码 `/unit_test`
这个是最难的命令要多次与LLM和用户交互。
第一步AST解析代码询问LLM想知道什么symbols析这些symbols放到contexts里第三步会用到
```python
"""
You're an advanced AI test generator.
You're about to write test cases for the function `{function_name}` in the file `{file_path}`.
Before you start, you need to check if you have enough context information to write the test cases.
Here is the source code of the function:
\```
{function_content}
\```
And here are some context information that might help you write the test cases:
{context_content}
Do you think the context information is enough?
If the information is insufficient, recommend which symbols or types you need to know more about.
Return a JSON object with a single key "key_symbols" whose value is a list of strings.
- If the context information is enough, return an empty list.
- Each string is the name of a symbol or type appearing in the function that lacks context information for writing test.
- The list should contain the most important symbols and should not exceed 10 items.
JSON Format Example:
{{
"key_symbols": ["<symbol 1>", "<symbol 2>", "<symbol 3>",...]
}}
"""
```
第二步询问LLM想知道什么文件第四步会用到
```python
"""
As an advanced AI coding assistant,
you're given the task to identify suitable reference test files that can be used as a guide
for writing test cases for a specific function in the codebase.
You're provided with a list of test files in the repository.
Infer the purpose of each test file and identify the top 3 key files
that may be relevant to the target function and can serve as a reference for writing test cases.
The reference could provide a clear example of best practices
in testing functions of a similar nature.
The target function is next_turn, located in the file .\example\game.py.
The list of test files in the repository is as follows:
- unit_tests\propose_test.py
- unit_tests\assistants\recommend_test_context.py
Answer in JSON format with a list of the top 3 key file paths under the key `files`.
Make sure each file path is from the list of test files provided above.
Example:
{
"files": ["<file path 1>", "<file path 2>", "<file path 3>"]
}
"""
```
第三步带上contexts向LLM询问测试方案
```python
"""
Help me write unit tests for the `{func_name}` function
"""
```
第四步app与用户交互用户提测试要求根据要求向LLM询问生成单元测试代码。
#### 1.2 生成代码文档 `/docstring`
类似`/explain`
#### 1.3 结论
devchat的vscode插件在算法上没有任何创新
1. devchat没有模型层面的优化
2. 静态的prompt模板除了`/unit_test`其他命令比较简单
3. 没有充分利用整个项目的信息
### 2 自研 ourchat
ourchat完全可以实现devchat并且效果不会比devchat差。
#### 2.1 测试环境
运行环境 CPU: 2.10GHz, 72核, GPU: tesla V100 16G
选同样的模型ourchat和devchat的输出几乎一样所以以下全是用ourchat测试
测试代码, 一个贪吃蛇游戏<https://github.com/autruonggiang/Simple-Snake-Game/blob/main/game.py>
```python
# Class to design the snake
class Snake:
...
# Class to design the food
class Food:
...
# Function to check the next move of snake
def next_turn(snake, food):
...
```
#### 2.2 测试生成代码块解释 `/explain`
使用snake game 的 next_turn 函数, 返回带上**think**部分以便分析在devchat是不显示的。
<style>
.float-container {
flex: 1;
display: flex;
}
.float-child {
width: 50%;
padding: 10px;
border: 2px solid red;
font-size: 12px;
flex: 1;
text-wrap: pretty;
word-wrap: break-word;
}
.float-header {
font-size: 20px;
align: center;
}
</style>
<div class="float-container">
<div class="float-child">
<div class="float-header">deepseek-r1:7b</div>
<br><b>think部分</b><br><br>
<p>
Okay, so I need to explain the selected portion of this Python code. Let me look at what's given.
</p><p>
The visible code starts from line 51 and goes up to line 116 in game.py. It includes a function next_turn and another part that defines change_direction and ions. I should focus on the if-else blocks within next_turn since that's where the logic for moving the snake, creating food, deleting objects, etc., happens
</p><p>
Let me break it down step by step.
</p><p>
First, in next_turn, they set x and y to the head of the snake's coordinates. Then there are conditionals checking the direction. Depending on whether it's ut, or right, x or y is adjusted by SPACE_SIZE. That makes sense because moving the head would mean updating its position.
</p><p>
Next, they insert the new head into snake.coordinates at position 0. So if the snake was [head, tail], it becomes [new_head, head, tail]. Then a rectangle (tody) is created and inserted at index 0. I suppose this is to make sure the drawing is in order.
</p><p>
Then there's an if statement checking if the current x and y match the food's coordinates. If they do, score increments by 1. The label updates with the new "food" is deleted from the canvas, and a new Food() object is created. This probably reappears when eaten.
</p><p>
Else, it deletes the last element of snake.coordinates (the tail) and also removes the last square from the snake's drawing. That makes sense to keep the gamthout memory issues.
</p><p>
After that, check_collisions(snake) is called. If true, game over; else, after some milliseconds, next_turn is scheduled again using window.after with SPEED .
</p><p>
Wait, but why do they call next_turn inside a conditional? Oh, because the window.after schedules it in the background so that the main loop can process evend creation). So when check_collisions returns true, the game ends without scheduling further turns.
</p><p>
tes direction based on new_direction input, handling reversals correctly. Check_collisions probably checks for wall or self-collision, which would trigger game over.
</p><p>
So putting it all together, this part of the code handles how the snake moves (based on direction), eats food to grow and increase score, clears old parts of the snake, and manages collisions to end the game.
</p>
-------------------------------------------------------------------------
<br><b>结论部分: </b><br><br>
<p>
To explain the selected portion of the code:
</p><p>
The provided Python code is an essential part of a Snake game implementation. It defines two functions (`next_turn` and `change_direction`) as well as two conditionals that handle key aspects of gameplay.
</p><p>
<b>### 1. **Function: next_turn**</b>
- Creates the core logic for moving and updating the snake's position.
- Takes parameters `snake` (the snake object) and `food` (a food object).
- It updates the snake's head based on its current direction:
- Changes x or y by `SPACE_SIZE` depending on direction ("up", "down", etc.).
- Appends the new head to the front of the snake's coordinates.
</p><p>
<b>### 2. **Drawing Elements**</b>
- Creates a rectangle (the snake's body segment) at the new head position.
- Adds this rectangle to the list of snake parts.
</p><p>
<b>### 3. **Food Interaction**</b>
- Checks if the snake has eaten the food by comparing positions.
- If eaten, score increases by 1, the score label updates, and a new food is generated.
- If not eaten, removes the last part of the snake (the tail) to maintain length.
</p><p>
<b>### 4. **Collision Detection**</b>
- Uses `check_collisions` function to detect:
- Collisions with walls or self-collision at the head.
- If collision detected, calls `game_over()` which stops the game by hiding windows.
</p><p>
<b>### 5. **Event Handling (Change Direction Function)**</b>
- Allows handling keyboard input for changing direction.
- Ensures smooth direction change by preventing double-reversals (e.g., not allowing turning left immediately after already going right).
</p><p>
<b>This logic ensures that:</b>
- The snake moves correctly in the grid based on direction.
- Food is consumed to grow and increase the score.
- Invalid moves like running into walls or self cause the game to end.
- Proper management of the snake's body segments as it grows.
</p><p>
The code is part of a loop where each call to `next_turn` can also schedule itself in a background thread (using `window.after`), allowing the main event loop time to handle food generation and other tasks.
</div>
<div class="float-child">
<div class="float-header">deepseek-r1:32b</div>
<br><b>think部分</b><br><br>
<p>
Alright, I need to explain the given code, specifically the selected portion around lines 48 to 93 of game.py. The visible code context includes a function called next_turn which takes snake and food as arguments.
</p><p>
First, looking at the code structure, I see that next_turn calculates new x and y coordinates based on the current direction. It then updates the snake's coordinate list by inserting the new position at the front. This suggests movement in the direction of heading.
</p><p>
Next, it creates a rectangle on the canvas with these new coordinates and adds this square to the snake's squares list. The use of insert(0, ...) implies that the first element is updated each turn, which aligns with how movement should happen—adding a new segment at the front.
</p><p>
The code then checks if the snake's head (x, y) matches the food's position. If it does, the score increments, updates the label, deletes the old food, and creates a new Food object. This handles when the snake eats food, growing itself by not popping the tail in this case.
</p><p>
If the snake doesn't eat food, the tail end of the coordinates list is removed. The canvas deletes the last square of the snake's body as well, giving the illusion of movement instead of just growing indefinitely.
</p><p>
After each move, check_collisions is called with the current snake state. If a collision is detected (either self-collision or hitting the window boundaries), game_over() is invoked to end the game.
</p><p>
If no collision occurs, the function schedules the next turn using window.after(SPEED, next_turn, snake, food). This sets up a recursive loop, allowing continuous movement of the snake at the defined speed.
</p><p>
Putting it all together, this function manages each step of the snake moving towards the next position based on the current direction. It updates both the internal state
</p><p>(coordinates) and the visual canvas to reflect this movement.
</p>
-------------------------------------------------------------------------
<br><b>结论部分: </b><br><br>
<p>
The code you've highlighted is responsible for controlling how the snake moves in each turn of a Snake game. Let's break down its functionality and logic:
</p><p>
<b>1. **Movement Calculation**:</b>
- The function begins by taking the current head position coordinates `(x, y)` from `snake.coordinates[0]`.
- Depending on the direction (`direction`), it adjusts these coordinates to move up, down, left, or right. This is done using conditional checks for each possible direction and adjusting either `x` or `y` by subtracting or adding `SPACE_SIZE`, which likely represents one segment of the grid or the snake's body.
</p><p>
<b>2. **Updating Snake Position**:</b>
- The new `(x, y)` coordinates are inserted at the beginning of `snake.coordinates` using `insert(0, (x, y))`. This adds a new head position in front of the snakes current position list.
- A new square (representing the snake's body part) is created on the canvas at the new `(x, y)` location using `canvas.create_rectangle()`, and this square is added to the front of `snake.squares` with `insert(0, square)`. This visually updates where the snake is moving to.
</p><p>
<b>3. **Eating Food**:</b>
- The code checks if the new head position matches the food's coordinates (`x == food.coordinates[0] and y == food.coordinates[1]`). If so:
- The score is incremented using `global score` and updated in the UI.
- The existing food image on the canvas is deleted with `canvas.delete("food")`, and a new `Food()` object is created to place the food anew somewhere else on the board. This effectively grows the snake by adding without removing the tail segment, so the snake's length increases.
</p><p>
<b>4. **No Food Eaten**:</b>
- If no food was eaten (`else` clause):
- The last x, y coordinates are removed from `snake.coordinates` using `del snake.coordinates[-1]`.
- The corresponding square (tail) is deleted from both the canvas and `snake.squares` by calling `canvas.delete(snake.squares[-1])` followed by `del snake.squares[-1]`.
- This action maintains the snake's length, making it appear as though the snake has moved forward; only its position is updated, not its size.
</p><p>
<b>5. **Collision Check**:</b>
- After each movement (whether or not food was eaten), it checks for collisions using `check_collisions(snake)`. If a collision is detected:
- The game ends by invoking `game_over()`.
- If no collision is detected, the function uses `window.after(SPEED, next_turn, snake, food)` to schedule the next turn after `SPEED` milliseconds. This creates a loop that continuously moves the snake at intervals specified in `SPEED`.
</p><p>
In summary, this code manages how the snake moves each turn:
- Moving the head based on current direction.
- Updating both the internal coordinate tracking and visual representation on the canvas
</p>
</div>
<div class="float-child blue">
<div class="float-header">deepseek-coder-v2:16b</div>
<br><b>think部分</b><br><br>
<p></p>
-------------------------------------------------------------------------
<br><b>结论部分: </b><br><br>
<p>
The selected code is the implementation of a game loop function named `next_turn` in Python. This function manages the progression of the snake's movement and interaction with food, as well as handling collisions to determine if the game should end. Here's a breakdown of its logic:
</p><p>
<b>1. **Extract Head Coordinates**:</b> The function starts by extracting the coordinates of the snake's head. These are obtained from `snake.coordinates[0]`.
</p><p>
<b>2. **Update Head Position Based on Direction**:</b> Depending on the current direction, it updates the x and y coordinates accordingly:
- If the direction is "up", it decreases the y-coordinate by `SPACE_SIZE`.
- If the direction is "down", it increases the y-coordinate by `SPACE_SIZE`.
- If the direction is "left", it decreases the x-coordinate by `SPACE_SIZE`.
- If the direction is "right", it increases the x-coordinate by `SPACE_SIZE`.
</p><p>
<b>3. **Insert New Head Position**:</b> It inserts the new coordinates (x, y) at the beginning of `snake.coordinates` to represent the snake's head moving in the specified direction.
</p><p>
<b>4. **Create a Rectangle for the New Head**:</b> Using the updated coordinates, it creates a rectangle on the canvas where the snake's head will be located with the size defined by `SPACE_SIZE`. This is done using `canvas.create_rectangle` and adds this square to both `snake.coordinates` and `snake.squares`.
</p><p>
<b>5. **Check for Food Consumption**:</b> It checks if the new head position coincides with the food's coordinates:
- If yes, it increments the score by 1 and updates the label displaying the score.
- It also removes the old food representation from the canvas (`canvas.delete("food")`) and generates a new food object at a random location.
- If no, it removes the last segment of the snake from both `snake.coordinates` and `snake.squares` by deleting them using `del`, effectively allowing
the snake to move without its tail following it.
</p><p>
<b>6. **Check for Collisions**:</b> After potentially updating or removing parts of the snake based on eating food, it checks if there are any collisions with walls or itself:
- If a collision is detected (indicated by the result of `check_collisions(snake)`), it calls the game-over function (`game_over()`).
- Otherwise, it schedules another call to `next_turn` after a delay specified by `SPEED`, passing along the same snake and food objects. This creates a loop that continuously updates the snake's movement every `SPEED` milliseconds until a termination condition is met (e.g., collision detected).
</p><p>
This function plays a critical role in maintaining game dynamics, such as moving the snake, checking for food consumption, updating scores, and detecting end conditions.
</p>
</div>
</div>
| 模型 | CPU 运行时间 | GPU 运行时间 |
|:--:|:--:|:--:|
| deepseek-r1:7b | 56.2s | 12.6s |
| deepseek-r1:32b | 399.7s | 72.3s |
| deepseek-coder-v2:16b | 47.4s | 12.6s |
***结论:***
1. 7B模型有单词拼错和语法不顺。
2. 输出受visible_text影响visible_text不是越多越好。
3. deepseek-coder 输出比较稳定,但总结能力偏弱。
#### 2.3 测试生成单元测试代码 `/init_test`
使用snake game 的 next_turn 函数。
<div class="float-container">
<div class="float-child">
<div class="float-header">deepseek-r1:32b</div>
<br><b>测试提议:</b><br>
<br>1. happy path: Test that the snake moves correctly when it eats food, increasing its length and score.
<br>2. happy path: Verify that the snake turns left without collision, updating its position and direction.
<br>3. happy path: Ensure the game continues smoothly when the snake moves right into empty space.
<br>4. edge case: Check if the game correctly handles a collision with the wall, triggering game over.
<br>5. edge case: Test that the snake dies when it collides with its own body after turning around.
<br>6. edge case: Verify that the score does not increase and the food remains when the snake moves without eating.
<br><br>
-------------------------------------------------------------------------
<br><b>测试代码: </b><br><br>
<p>
```python
import unittest
from unittest.mock import patch, MagicMock
class TestNextTurn(unittest.TestCase):
def setUp(self):
# Initialize mocks for global variables
self.canvas = MagicMock()
self.snake = {'coordinates': [], 'direction': None}
self.food = {'x': 0, 'y': 0}
self.score = 0
self.game_over = MagicMock()
@patch('builtins.print') # Mock print function if used in next_turn
def test_eat_food_increases_length_and_score(self, mock_print):
"""Test that the snake moves correctly when it eats food, increasing its length and score."""
# Setup initial state
self.snake['coordinates'] = [(100, 100)]
self.snake['direction'] = "right"
self.food['x'], self.food['y'] = 110, 100
next_turn()
# Assert the snake's length increased
self.assertEqual(len(self.snake['coordinates']), 2)
# Assert score increased
self.assertEqual(self.score, 1)
# Assert new food was created
self.food.assert_called_once()
@patch('builtins.print')
def test_turn_left_without_collision(self, mock_print):
"""Verify that the snake turns left without collision, updating its position and direction."""
# Setup initial state
self.snake['coordinates'] = [(100, 100)]
self.snake['direction'] = "up"
next_turn()
# Assert the snake's direction changed
self.assertEqual(self.snake['direction'], "left")
# Assert position updated correctly (example coordinates)
self.assertEqual(self.snake['coordinates'][0], (90, 100))
@patch('builtins.print')
def test_move_right_into_empty_space(self, mock_print):
"""Ensure the game continues smoothly when the snake moves right into empty space."""
# Setup initial state
self.snake['coordinates'] = [(100, 100)]
self.snake['direction'] = "right"
global direction
direction = "right"
next_turn()
# Assert position updated correctly without increasing length
self.assertEqual(len(self.snake['coordinates']), 1)
self.assertEqual(self.snake['coordinates'][0], (110, 100))
@patch('builtins.print')
def test_wall_collision_triggers_game_over(self, mock_print):
"""Test that wall collision triggers game over."""
# Setup initial state at edge of canvas
self.snake['coordinates'] = [(0, 100)]
self.snake['direction'] = "left"
next_turn()
# Assert game_over function was called
self.game_over.assert_called_once()
@patch('builtins.print')
def test_self_collision_triggers_game_over(self, mock_print):
"""Test that self collision triggers game over."""
# Setup initial state with overlapping coordinates
self.snake['coordinates'] = [(100, 100), (90, 100)]
self.snake['direction'] = "right"
next_turn()
# Assert game_over function was called
self.game_over.assert_called_once()
@patch('builtins.print')
def test_move_without_eating_food(self, mock_print):
"""Test that moving without eating food does not change the score."""
# Setup initial state
self.snake['coordinates'] = [(100, 100)]
self.snake['direction'] = "right"
global direction
direction = "right"
next_turn()
# Assert score remains unchanged
self.assertEqual(self.score, 0)
# Assert food was not recreated
self.food.assert_not_called()
if __name__ == '__main__':
unittest.main()
```
### Explanation
- **Mocking**: The solution uses `unittest.mock` to replace global variables with mock objects, isolating the function under test.
- **Setup and Teardown**: Each test method sets up specific initial states using `setUp()`, ensuring tests are independent.
- **Assertions**: After invoking `next_turn`, each test asserts expected outcomes such as state changes, function calls, and score updates.
This approach ensures that all critical scenarios are tested efficiently, providing confidence in the function's correctness.
</div>
<div class="float-child">
<div class="float-header">deepseek-coder-v2:16b</div>
<br><b>测试提议:</b><br>
<br>1. happy path: Test the next_turn function with a simple move up scenario where the snake eats food and grows longer.
<br>2. happy path: Test the next_turn function when the snake moves right but does not eat any food, causing it to just move without growing.
<br>3. edge case: Test the next_turn function with a scenario where the snake moves down and hits the boundary of the game area, triggering game over.
<br>4. edge case: Test the next_turn function when there is no food present at the new head position of the snake, causing the tail to be removed without growing.
<br>5. edge case: Test the next_turn function with a scenario where the snake moves left and hits another part of its body, triggering game over.
<br><br>
-------------------------------------------------------------------------
<br><b>测试代码: </b><br><br>
```python
import unittest
from unittest.mock import patch, MagicMock
import random
# Assuming SPACE_SIZE, WIDTH, HEIGHT, SNAKE, FOOD are defined somewhere in the game.py file
SPACE_SIZE = 20
WIDTH = 400
HEIGHT = 400
SNAKE = "green"
FOOD = "red"
SPEED = 100
class TestNextTurn(unittest.TestCase):
def setUp(self):
# Mocking the necessary components for testing
self.snake_mock = MagicMock()
self.food_mock = MagicMock()
self.canvas_mock = MagicMock()
self.label_mock = MagicMock()
self.score = 0
self.direction = "up"
def test_happy_path_snake_eats_food(self):
# TODO: Implement the happy path scenario where the snake moves up and eats food, causing it to grow longer.
pass
def test_happy_path_snake_moves_right_no_eat(self):
# TODO: Implement the happy path scenario where the snake moves right but does not eat any food, causing it to just move without growing.
pass
def test_edge_case_snake_hits_boundary(self):
# TODO: Implement the edge case scenario where the snake moves down and hits the boundary of the game area, triggering game over.
pass
def test_edge_case_no_food_at_new_head(self):
# TODO: Implement the edge case scenario when there is no food present at the new head position of the snake, causing the tail to be removed without growing.
pass
def test_edge_case_snake_hits_itself(self):
# TODO: Implement the edge case scenario where the snake moves left and hits another part of its body, triggering game over.
pass
if __name__ == "__main__":
unittest.main()
```
This code sets up a basic test suite for the `next_turn` function in the `game.py` file. Each test case is marked with TODO comments to indicate where specific implementation details need to be added. The setup method initializes mock objects for snake, food, canvas, and label, which are necessary for testing the function.
</div>
</div>
<div class="float-container">
<div class="float-child">
<div class="float-header">deepseek-v2:236b</div>
<br><b>测试提议:</b><br>
<br>1. happy path: Test that the snake moves correctly when it eats food, increasing its length and score.
<br>2. happy path: Verify that the game continues after a normal move without eating food or colliding.
<br>3. happy path: Ensure that the snake's head moves in the correct direction based on the current direction setting.
<br>4. edge case: Check if the game correctly ends when the snake hits a wall boundary.
<br>5. edge case: Test that the game terminates when the snake collides with its own body.
<br>6. edge case: Verify that the function handles unexpected direction values gracefully without crashing.
<br><br>
-------------------------------------------------------------------------
<br><b>测试代码: </b><br><br>
```python
import pytest
from unittest.mock import Mock, patch
from example.game import next_turn, check_collisions, Food
class TestNextTurn:
def setup_method(self):
# Initialize mock objects shared across tests
self.snake = Mock()
self.food = Mock()
self.window = Mock()
self.canvas = Mock()
# Setup initial conditions for snake and food
self.snake.coordinates = [(0, 0)]
self.food.coordinates = [Mock(), Mock()] # x and y coordinates
# Mock global variables
self.mock_score = 0
self.mock_direction = "right"
self.SPEED = 100
self.SPACE_SIZE = 20
self.WIDTH = 800
self.HEIGHT = 600
@patch('example.game.score', 0)
def test_happy_path_eat_food(self, mock_score):
# Setup: Snake eats food
self.snake.coordinates[0] = (self.food.coordinates[0],
self.food.coordinates[1])
next_turn(self.snake, self.food, window=self.window, canvas=self.canvas)
# Assert score increases and new segment is added
assert mock_score == 1
assert len(self.snake.coordinates) > 1
@patch('example.game.score', 0)
def test_happy_path_grow_segment(self, mock_score):
# Setup: Snake eats food
self.snake.coordinates[0] = (self.food.coordinates[0],
self.food.coordinates[1])
next_turn(self.snake, self.food, window=self.window, canvas=self.canvas)
# Assert snake grows by one segment
assert len(self.snake.coordinates) == 2
@patch('example.game.window')
def test_happy_path_schedule_next_turn(self, mock_window):
# Setup: No collision
next_turn(self.snake, self.food, window=mock_window, canvas=self.canvas)
# Assert window.after is called with SPEED
mock_window.after.assert_called_once_with(self.SPEED,
next_turn,
self.ref_args(),
self.ref_kwargs())
@patch('example.game.game_over')
def test_edge_case_self_collision(self, mock_game_over):
# Setup: Snake collides with itself
self.snake.coordinates = [(0, 0), (0, 0)]
next_turn(self.snake, self.food, window=self.window, canvas=self.canvas)
# Assert game over is called
mock_game_over.assert_called_once()
@patch('example.game.direction')
def test_edge_case_all_directions(self, mock_direction):
directions = ["up", "down", "left", "right"]
for direction in directions:
mock_direction.return_value = direction
next_turn(self.snake, self.food, window=self.window, canvas=self.canvas)
# Verify movement in each direction
if direction == "up":
assert self.snake.coordinates[0][1] < 0
elif direction == "down":
assert self.snake.coordinates[0][1] > 0
elif direction == "left":
assert self.snake.coordinates[0][0] < 0
else: # right
assert self.snake.coordinates[0][0] > 0
@patch('example.game.game_over')
def test_edge_case_out_of_bounds(self, mock_game_over):
# Setup: Snake moves out of bounds
self.snake.coordinates = [(self.WIDTH + 1, 0)]
next_turn(self.snake, self.food, window=self.window, canvas=self.canvas)
# Assert game over is called
mock_game_over.assert_called_once()
def main():
pytest.main([__file__])
if __name__ == "__main__":
main()
```
This test suite:
1. Uses pytest and unittest.mock for mocking dependencies
2. Tests all six requested scenarios
3. Includes proper setup and teardown with fixture methods
4. Mocks external dependencies like window, canvas, and game state
5. Verifies both successful paths and edge cases
6. Follows best practices for test organization and structure
To run the tests:
```bash
python -m pytest test_game.py -v
```
Note: This implementation assumes some knowledge of the internal implementation details of the game. You may need to adjust mocks based on actual implementation specifics.
</div>
<div class="float-child">
<div class="float-header">deepseek-coder-v2:236b</div>
<br><b>测试提议:</b><br>
<br>1. happy path: Test that the snake moves up correctly when direction is 'up'.
<br>2. happy path: Verify that the score increments and food regenerates when the snake eats the food.
<br>3. happy path: Check that the game continues with updated positions if no collision occurs after a move.
<br>4. edge case: Ensure game over is triggered when the snake collides with the boundary of the game area.
<br>5. edge case: Test that the game correctly handles a collision where the snake's head hits its own body part.
<br>6. edge case: Verify that the game over screen is displayed with correct text and formatting when a game over condition is met.
<br><br>
-------------------------------------------------------------------------
<br><b>测试代码: </b><br><br>
<p>
```python
import unittest
from unittest.mock import MagicMock, patch
# Assuming these imports are from the actual game module
from example.game import next_turn, check_collisions, Food, Snake, canvas, label, window, SNAKE, SPACE_SIZE, WIDTH, HEIGHT, SPEED, score
class TestGame(unittest.TestCase):
def setUp(self):
self.snake = MagicMock()
self.food = MagicMock()
self.canvas = MagicMock()
self.label = MagicMock()
self.window = MagicMock()
# happy path: Test that the snake moves up correctly when direction is 'up'.
@patch('example.game.direction', 'up')
def test_snake_moves_up(self):
initial_coordinates = [(10, 10)]
self.snake.coordinates = initial_coordinates.copy()
next_turn(self.snake, self.food)
expected_coordinates = [(10, 10 - SPACE_SIZE)]
self.assertEqual(self.snake.coordinates[0], expected_coordinates[0])
# happy path: Verify that the score increments and food regenerates when the snake eats the food.
@patch('example.game.direction', 'up')
def test_score_increments_and_food_regenerates(self):
initial_coordinates = [(10, 10)]
self.snake.coordinates = initial_coordinates.copy()
self.food.coordinates = [10, 10 - SPACE_SIZE]
next_turn(self.snake, self.food)
self.assertEqual(score, 1)
self.food.assert_called()
# happy path: Check that the game continues with updated positions if no collision occurs after a move.
@patch('example.game.direction', 'up')
def test_game_continues_with_updated_positions(self):
initial_coordinates = [(10, 10)]
self.snake.coordinates = initial_coordinates.copy()
next_turn(self.snake, self.food)
expected_coordinates = [(10, 10 - SPACE_SIZE)]
self.assertEqual(self.snake.coordinates[0], expected_coordinates[0])
self.window.after.assert_called_with(SPEED, next_turn, self.snake, self.food)
# edge case: Ensure game over is triggered when the snake collides with the boundary of the game area.
@patch('example.game.direction', 'up')
def test_game_over_on_boundary_collision(self):
initial_coordinates = [(10, 0)]
self.snake.coordinates = initial_coordinates.copy
</div>
</div>
| 模型 | CPU 运行时间 | GPU 运行时间 |
|:--:|:--:|:--:|
| deepseek-coder-v2:16b | - | 18.3s |
| deepseek-r1:32b | - | 213.4s |
| deepseek-v2:236b | 644.5s | - |
| deepseek-coder-v2:236b | 527.5s | - |
***结论:***
1. deepseek-r1:7b和deepseek-r1:70b没有输出测试方案。
2. deepseek-coder的推理能力太弱了但是编程语法正确。
3. r1和v2的整体逻辑较强但是编程语法错误。

16
examples/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"dependencies": {
"@types/node": "^22.13.13",
"tslib": "^2.8.1",
"ws": "^8.18.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.10",
"@types/ws": "^8.18.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.3.0"
}
}

View File

@ -0,0 +1,33 @@
import ws from 'ws';
process.env["NODE_TLS_REJECT_UNAUTHORIZED"]="0"
let good = new ws.WebSocket('wss://127.0.0.1:8080/ws', {headers: {
["X-Api-Key"]: "simpletest2025_001",
}})
good.on('error', console.error)
good.on('open', ()=>{
console.log('good connected')
})
good.on('close', ()=>{
console.log('good disconnected')
})
let bad = new ws.WebSocket('wss://127.0.0.1:8080/ws')
bad.on('message', (data)=>{
console.log("agent -> bad:")
console.log(data.toString())
})
bad.on('error', console.error)
bad.on('open', ()=>{
console.log('bad connected')
})
bad.on('close', ()=>{
console.log('bad disconnected')
})

36
examples/src/common.ts Normal file
View File

@ -0,0 +1,36 @@
import ws from 'ws';
process.env["NODE_TLS_REJECT_UNAUTHORIZED"]="0"
const ADDR = "wss://127.0.0.1:8080/ws"
export function makeClient(
onMessage: ((data: ws.RawData) => void) | any = undefined,
) : Promise<ws.WebSocket> {
return new Promise((resolve, reject) => {
let client = new ws.WebSocket(ADDR, {headers: {
["X-Api-Key"]: "simpletest2025_001",
}})
client.on('message', (data)=>{
if (onMessage != undefined) {
onMessage(data)
} else {
console.log("agent -> me:")
console.log(data.toString())
}
})
client.on('error', console.error)
client.on('open', ()=>{
console.log('connected')
resolve(client)
})
client.on('close', ()=>{
console.log('disconnected')
})
})
}
export function sleep(ms: number) : Promise<any> {
return new Promise( resolve => setTimeout(resolve, ms))
}

40
examples/src/exec_chat.ts Normal file
View File

@ -0,0 +1,40 @@
import ws from 'ws';
import { makeClient } from "./common";
async function run() {
let client = await makeClient()
let req = {
"cmd" : "exec_chat",
"request_id" : 123, // 随机生成 (必填)
"language" : "en", // 语言, en-英文, cn-中文,默认为 en (可选)
"msg" : "What would be a good company name a company that makes colorful socks?",
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false // stream方式返回目前未支持 (可选)
}
client.send(JSON.stringify(req))
}
async function run_stream() {
let client = await makeClient((data: ws.RawData)=>{
let d = JSON.parse(data.toString())
process.stdout.write(d.msg)
console.log(data.toString())
return
})
let req = {
"cmd" : "exec_chat",
"request_id" : 123, // 随机生成 (必填)
"language" : "en", // 语言, en-英文, cn-中文,默认为 en (可选)
"msg" : "What would be a good company name a company that makes colorful socks?",
"model" : "local deepseek-r1:32b", //(必填)
"stream" : true // stream方式返回目前未支持 (可选)
}
client.send(JSON.stringify(req))
}
run_stream()

View File

@ -0,0 +1,25 @@
import { makeClient } from "./common";
async function run() {
let client = await makeClient()
let selected_text = {
"filepath": "example\\\\game.py", // 文件路径
"range": {
"start": { "line": 33, "character": 0}, // 从33行0字符开始
"end" : { "line": 45, "character": 41} // 到45行41字符结束
},
"text": `def change_direction(new_direction): \n\n\tglobal direction \n\n\tif new_direction == 'left': \n\t\tif direction != 'right': \n\t\t\tdirection = new_direction \n\telif new_direction == 'right': \n\t\tif direction != 'left': \n\t\t\tdirection = new_direction \n\telif new_direction == 'up': \n\t\tif direction != 'down': \n\t\t\tdirection = new_direction \n\telif new_direction == 'down': \n\t\tif direction != 'up': \n\t\t\tdirection = new_direction` // 源码片段
}
let req = {
"cmd" : "exec_docstring",
"request_id": 123, // 随机生成 (必填)
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false,
"selected_text": selected_text,
}
client.send(JSON.stringify(req))
}
run()

View File

@ -0,0 +1,35 @@
import { makeClient } from "./common";
async function run() {
let client = await makeClient()
let selected_text = {
"filepath": "example\\\\game.py", // 文件路径
"range": {
"start": { "line": 33, "character": 0}, // 从33行0字符开始
"end" : { "line": 45, "character": 41} // 到45行41字符结束
},
"text": `class Food: \n\n\tdef __init__(self): \n\n\t\tx = random.randint(0, \n\t\t\t\t(WIDTH / SPACE_SIZE)-1) * SPACE_SIZE \n\t\ty = random.randint(0, \n\t\t\t\t(HEIGHT / SPACE_SIZE) - 1) * SPACE_SIZE \n\n\t\tself.coordinates = [x, y] \n\n\t\tcanvas.create_oval(x, y, x + SPACE_SIZE, y +\n\t\t\t\t\t\tSPACE_SIZE, fill=FOOD, tag="food") ` // 源码片段
}
let visible_text = {
"filepath": "g:\\\\workspace\\\\example\\\\game.py",
"range": {
"start": { "line": 6, "character": 0},
"end" : { "line": 69, "character": 0}
},
"text": `WIDTH = 500\nHEIGHT = 500\nSPEED = 200\nSPACE_SIZE = 20\nBODY_SIZE = 2\nSNAKE = "#0000FF"\nFOOD = "#FFFF00"\nBACKGROUND = "#000000"\n\n# Class to design the snake \nclass Snake: \n\n\tdef __init__(self): \n\t\tself.body_size = BODY_SIZE \n\t\tself.coordinates = [] \n\t\tself.squares = [] \n\n\t\tfor i in range(0, BODY_SIZE): \n\t\t\tself.coordinates.append([0, 0]) \n\n\t\tfor x, y in self.coordinates: \n\t\t\tsquare = canvas.create_rectangle( \n\t\t\t\tx, y, x + SPACE_SIZE, y + SPACE_SIZE, \n\t\t\t\t\tfill=SNAKE, tag="snake") \n\t\t\tself.squares.append(square) \n\n# Class to design the food \nclass Food: \n\n\tdef __init__(self): \n\n\t\tx = random.randint(0, \n\t\t\t\t(WIDTH / SPACE_SIZE)-1) * SPACE_SIZE \n\t\ty = random.randint(0, \n\t\t\t\t(HEIGHT / SPACE_SIZE) - 1) * SPACE_SIZE \n\n\t\tself.coordinates = [x, y] \n\n\t\tcanvas.create_oval(x, y, x + SPACE_SIZE, y +\n\t\t\t\t\t\tSPACE_SIZE, fill=FOOD, tag="food") \n\n# Function to check the next move of snake \ndef next_turn(snake, food): \n\n\tx, y = snake.coordinates[0] \n\n\tif direction == "up": \n\t\ty -= SPACE_SIZE \n\telif direction == "down": \n\t\ty += SPACE_SIZE \n\telif direction == "left": \n\t\tx -= SPACE_SIZE \n\telif direction == "right": \n\t\tx += SPACE_SIZE \n\n\tsnake.coordinates.insert(0, (x, y)) \n\n\tsquare = canvas.create_rectangle( \n\t\tx, y, x + SPACE_SIZE, \n\t\t\t\ty + SPACE_SIZE, fill=SNAKE) \n\n\tsnake.squares.insert(0, square) \n\n\tif x == food.coordinates[0] and y == food.coordinates[1]: `
}
let req = {
"cmd" : "exec_explain",
"request_id": 123, // 随机生成 (必填)
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false,
"selected_text": selected_text,
"visible_text" : visible_text
}
client.send(JSON.stringify(req))
}
run()

31
examples/src/exec_fix.ts Normal file
View File

@ -0,0 +1,31 @@
import ws from 'ws';
import { makeClient } from "./common";
async function run() {
let client = await makeClient((data: ws.RawData)=>{
let d = JSON.parse(data.toString())
console.log("------------------------------- msg start -------------------------------")
console.log(d.msg)
console.log("------------------------------- msg end -------------------------------")
})
let selected_text = {
"filepath": "example\\\\game.py", // 文件路径
"range": {
"start": { "line": 33, "character": 0}, // 从33行0字符开始
"end" : { "line": 45, "character": 41} // 到45行41字符结束
},
"text": `def change_direction(new_direction): \n\n\tglobal direction \n\n\tif new_direction == 'left': \n\t\tif direction != 'right': \n\t\t\tdirection = new_direction \n\telif new_direction == 'right': \n\t\tif direction != 'left': \n\t\t\tdirection = new_direction \n\telif new_direction == 'up': \n\t\tif direction != 'down': \n\t\t\tdirection = new_direction \n\telif new_direction == 'down': \n\t\tif direction != 'up': \n\t\t\tdirection = new_direction` // 源码片段
}
let req = {
"cmd" : "exec_fix",
"request_id": 123, // 随机生成 (必填)
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false,
"selected_text": selected_text,
}
client.send(JSON.stringify(req))
}
run()

View File

@ -0,0 +1,31 @@
import ws from 'ws';
import { makeClient } from "./common";
async function run() {
let client = await makeClient((data: ws.RawData)=>{
let d = JSON.parse(data.toString())
console.log("------------------------------- msg start -------------------------------")
console.log(d.msg)
console.log("------------------------------- msg end -------------------------------")
})
let selected_text = {
"filepath": "example\\\\game.py", // 文件路径
"range": {
"start": { "line": 33, "character": 0}, // 从33行0字符开始
"end" : { "line": 45, "character": 41} // 到45行41字符结束
},
"text": `def change_direction(new_direction): \n\n\tglobal direction \n\n\tif new_direction == 'left': \n\t\tif direction != 'right': \n\t\t\tdirection = new_direction \n\telif new_direction == 'right': \n\t\tif direction != 'left': \n\t\t\tdirection = new_direction \n\telif new_direction == 'up': \n\t\tif direction != 'down': \n\t\t\tdirection = new_direction \n\telif new_direction == 'down': \n\t\tif direction != 'up': \n\t\t\tdirection = new_direction` // 源码片段
}
let req = {
"cmd" : "exec_optimize",
"request_id": 123, // 随机生成 (必填)
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false,
"selected_text": selected_text,
}
client.send(JSON.stringify(req))
}
run()

View File

@ -0,0 +1,31 @@
import ws from 'ws';
import { makeClient } from "./common";
async function run() {
let client = await makeClient((data: ws.RawData)=>{
let d = JSON.parse(data.toString())
console.log("------------------------------- msg start -------------------------------")
console.log(d.msg)
console.log("------------------------------- msg end -------------------------------")
})
let selected_text = {
"filepath": "example\\\\game.py", // 文件路径
"range": {
"start": { "line": 33, "character": 0}, // 从33行0字符开始
"end" : { "line": 45, "character": 41} // 到45行41字符结束
},
"text": `def change_direction(new_direction): \n\n\tglobal direction \n\n\tif new_direction == 'left': \n\t\tif direction != 'right': \n\t\t\tdirection = new_direction \n\telif new_direction == 'right': \n\t\tif direction != 'left': \n\t\t\tdirection = new_direction \n\telif new_direction == 'up': \n\t\tif direction != 'down': \n\t\t\tdirection = new_direction \n\telif new_direction == 'down': \n\t\tif direction != 'up': \n\t\t\tdirection = new_direction` // 源码片段
}
let req = {
"cmd" : "exec_fix",
"request_id": 123, // 随机生成 (必填)
"model" : "local deepseek-r1:32b", //(必填)
"stream" : false,
"selected_text": selected_text,
}
client.send(JSON.stringify(req))
}
run()

View File

@ -0,0 +1,13 @@
import { makeClient } from "./common";
async function run() {
let client = await makeClient()
let req = {
"cmd" : "list_model",
"request_id": 123, // 随机生成 (必填)
}
client.send(JSON.stringify(req))
}
run()

744
examples/src/symbol.json Normal file
View File

@ -0,0 +1,744 @@
[
{
"name": "WIDTH",
"kind": "Constant",
"range": {
"start": {
"line": 6,
"character": 0
},
"end": {
"line": 6,
"character": 5
}
},
"children": []
},
{
"name": "HEIGHT",
"kind": "Constant",
"range": {
"start": {
"line": 7,
"character": 0
},
"end": {
"line": 7,
"character": 6
}
},
"children": []
},
{
"name": "SPEED",
"kind": "Constant",
"range": {
"start": {
"line": 8,
"character": 0
},
"end": {
"line": 8,
"character": 5
}
},
"children": []
},
{
"name": "SPACE_SIZE",
"kind": "Constant",
"range": {
"start": {
"line": 9,
"character": 0
},
"end": {
"line": 9,
"character": 10
}
},
"children": []
},
{
"name": "BODY_SIZE",
"kind": "Constant",
"range": {
"start": {
"line": 10,
"character": 0
},
"end": {
"line": 10,
"character": 9
}
},
"children": []
},
{
"name": "SNAKE",
"kind": "Constant",
"range": {
"start": {
"line": 11,
"character": 0
},
"end": {
"line": 11,
"character": 5
}
},
"children": []
},
{
"name": "FOOD",
"kind": "Constant",
"range": {
"start": {
"line": 12,
"character": 0
},
"end": {
"line": 12,
"character": 4
}
},
"children": []
},
{
"name": "BACKGROUND",
"kind": "Constant",
"range": {
"start": {
"line": 13,
"character": 0
},
"end": {
"line": 13,
"character": 10
}
},
"children": []
},
{
"name": "Snake",
"kind": "Class",
"range": {
"start": {
"line": 16,
"character": 0
},
"end": {
"line": 30,
"character": 30
}
},
"children": [
{
"name": "__init__",
"kind": "Method",
"range": {
"start": {
"line": 18,
"character": 1
},
"end": {
"line": 30,
"character": 30
}
},
"children": [
{
"name": "i",
"kind": "Variable",
"range": {
"start": {
"line": 23,
"character": 6
},
"end": {
"line": 23,
"character": 7
}
},
"children": []
},
{
"name": "x",
"kind": "Variable",
"range": {
"start": {
"line": 26,
"character": 6
},
"end": {
"line": 26,
"character": 7
}
},
"children": []
},
{
"name": "y",
"kind": "Variable",
"range": {
"start": {
"line": 26,
"character": 9
},
"end": {
"line": 26,
"character": 10
}
},
"children": []
},
{
"name": "square",
"kind": "Variable",
"range": {
"start": {
"line": 27,
"character": 3
},
"end": {
"line": 27,
"character": 9
}
},
"children": []
}
]
},
{
"name": "body_size",
"kind": "Variable",
"range": {
"start": {
"line": 19,
"character": 7
},
"end": {
"line": 19,
"character": 16
}
},
"children": []
},
{
"name": "coordinates",
"kind": "Variable",
"range": {
"start": {
"line": 20,
"character": 7
},
"end": {
"line": 20,
"character": 18
}
},
"children": []
},
{
"name": "squares",
"kind": "Variable",
"range": {
"start": {
"line": 21,
"character": 7
},
"end": {
"line": 21,
"character": 14
}
},
"children": []
}
]
},
{
"name": "Food",
"kind": "Class",
"range": {
"start": {
"line": 33,
"character": 0
},
"end": {
"line": 45,
"character": 40
}
},
"children": [
{
"name": "__init__",
"kind": "Method",
"range": {
"start": {
"line": 35,
"character": 1
},
"end": {
"line": 45,
"character": 40
}
},
"children": [
{
"name": "x",
"kind": "Variable",
"range": {
"start": {
"line": 37,
"character": 2
},
"end": {
"line": 37,
"character": 3
}
},
"children": []
},
{
"name": "y",
"kind": "Variable",
"range": {
"start": {
"line": 39,
"character": 2
},
"end": {
"line": 39,
"character": 3
}
},
"children": []
}
]
},
{
"name": "coordinates",
"kind": "Variable",
"range": {
"start": {
"line": 42,
"character": 7
},
"end": {
"line": 42,
"character": 18
}
},
"children": []
}
]
},
{
"name": "next_turn",
"kind": "Function",
"range": {
"start": {
"line": 48,
"character": 0
},
"end": {
"line": 93,
"character": 45
}
},
"children": [
{
"name": "snake",
"kind": "Variable",
"range": {
"start": {
"line": 48,
"character": 14
},
"end": {
"line": 48,
"character": 19
}
},
"children": []
},
{
"name": "food",
"kind": "Variable",
"range": {
"start": {
"line": 48,
"character": 21
},
"end": {
"line": 48,
"character": 25
}
},
"children": []
},
{
"name": "x",
"kind": "Variable",
"range": {
"start": {
"line": 50,
"character": 1
},
"end": {
"line": 50,
"character": 2
}
},
"children": []
},
{
"name": "y",
"kind": "Variable",
"range": {
"start": {
"line": 50,
"character": 4
},
"end": {
"line": 50,
"character": 5
}
},
"children": []
},
{
"name": "square",
"kind": "Variable",
"range": {
"start": {
"line": 63,
"character": 1
},
"end": {
"line": 63,
"character": 7
}
},
"children": []
}
]
},
{
"name": "change_direction",
"kind": "Function",
"range": {
"start": {
"line": 96,
"character": 0
},
"end": {
"line": 111,
"character": 28
}
},
"children": [
{
"name": "new_direction",
"kind": "Variable",
"range": {
"start": {
"line": 96,
"character": 21
},
"end": {
"line": 96,
"character": 34
}
},
"children": []
}
]
},
{
"name": "check_collisions",
"kind": "Function",
"range": {
"start": {
"line": 114,
"character": 0
},
"end": {
"line": 127,
"character": 13
}
},
"children": [
{
"name": "snake",
"kind": "Variable",
"range": {
"start": {
"line": 114,
"character": 21
},
"end": {
"line": 114,
"character": 26
}
},
"children": []
},
{
"name": "x",
"kind": "Variable",
"range": {
"start": {
"line": 116,
"character": 1
},
"end": {
"line": 116,
"character": 2
}
},
"children": []
},
{
"name": "y",
"kind": "Variable",
"range": {
"start": {
"line": 116,
"character": 4
},
"end": {
"line": 116,
"character": 5
}
},
"children": []
},
{
"name": "body_part",
"kind": "Variable",
"range": {
"start": {
"line": 123,
"character": 5
},
"end": {
"line": 123,
"character": 14
}
},
"children": []
}
]
},
{
"name": "game_over",
"kind": "Function",
"range": {
"start": {
"line": 130,
"character": 0
},
"end": {
"line": 137,
"character": 20
}
},
"children": []
},
{
"name": "window",
"kind": "Variable",
"range": {
"start": {
"line": 142,
"character": 0
},
"end": {
"line": 142,
"character": 6
}
},
"children": []
},
{
"name": "score",
"kind": "Variable",
"range": {
"start": {
"line": 146,
"character": 0
},
"end": {
"line": 146,
"character": 5
}
},
"children": []
},
{
"name": "direction",
"kind": "Variable",
"range": {
"start": {
"line": 147,
"character": 0
},
"end": {
"line": 147,
"character": 9
}
},
"children": []
},
{
"name": "label",
"kind": "Variable",
"range": {
"start": {
"line": 151,
"character": 0
},
"end": {
"line": 151,
"character": 5
}
},
"children": []
},
{
"name": "canvas",
"kind": "Variable",
"range": {
"start": {
"line": 155,
"character": 0
},
"end": {
"line": 155,
"character": 6
}
},
"children": []
},
{
"name": "window_width",
"kind": "Variable",
"range": {
"start": {
"line": 161,
"character": 0
},
"end": {
"line": 161,
"character": 12
}
},
"children": []
},
{
"name": "window_height",
"kind": "Variable",
"range": {
"start": {
"line": 162,
"character": 0
},
"end": {
"line": 162,
"character": 13
}
},
"children": []
},
{
"name": "screen_width",
"kind": "Variable",
"range": {
"start": {
"line": 163,
"character": 0
},
"end": {
"line": 163,
"character": 12
}
},
"children": []
},
{
"name": "screen_height",
"kind": "Variable",
"range": {
"start": {
"line": 164,
"character": 0
},
"end": {
"line": 164,
"character": 13
}
},
"children": []
},
{
"name": "x",
"kind": "Variable",
"range": {
"start": {
"line": 166,
"character": 0
},
"end": {
"line": 166,
"character": 1
}
},
"children": []
},
{
"name": "y",
"kind": "Variable",
"range": {
"start": {
"line": 167,
"character": 0
},
"end": {
"line": 167,
"character": 1
}
},
"children": []
},
{
"name": "snake",
"kind": "Variable",
"range": {
"start": {
"line": 180,
"character": 0
},
"end": {
"line": 180,
"character": 5
}
},
"children": []
},
{
"name": "food",
"kind": "Variable",
"range": {
"start": {
"line": 181,
"character": 0
},
"end": {
"line": 181,
"character": 4
}
},
"children": []
}
]

28
examples/tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compileOnSave": false,
"exclude": ["src/**/*.test.ts", "test/*"],
"compilerOptions": {
"baseUrl": "./",
"module": "commonjs",
"target": "es2019",
"outDir": "./dist",
"sourceMap": true,
"declaration": true,
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"importHelpers": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": false,
"noImplicitReturns": false,
"noUnusedParameters": false,
"resolveJsonModule": true,
"strictNullChecks": true,
"typeRoots": ["node_modules/@types"],
"lib": ["es2018", "dom"],
},
"include": ["src/**/*.ts"],
"types": ["jest", "node"]
}

12
examples/tslint.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": ["tslint-config-prettier"],
"rulesDirectory": ["tslint-plugin-prettier"],
"rules": {
"prettier": true,
"max-line-length": [true, 100],
"import-name": false
},
"linterOptions": {
"exclude": ["src/**"]
}
}

28
go.mod Normal file
View File

@ -0,0 +1,28 @@
module agent
go 1.23.0
toolchain go1.23.7
require (
github.com/fatih/color v1.18.0
github.com/go-stack/stack v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f
github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-isatty v0.0.20
github.com/quasilyte/go-ruleguard/dsl v0.3.22
github.com/tmc/langchaingo v0.1.13
golang.org/x/sys v0.31.0
golang.org/x/time v0.11.0
)
require (
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/sync v0.12.0 // indirect
)

52
go.sum Normal file
View File

@ -0,0 +1,52 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

86
rules.go Normal file
View File

@ -0,0 +1,86 @@
//go:build gorules
package gorules
// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module
// to apply changes in this file, please do: ./build/bin/golangci-lint cache clean
import (
"github.com/quasilyte/go-ruleguard/dsl"
//quasilyterules "github.com/quasilyte/ruleguard-rules-test"
)
func init() {
//dsl.ImportRules("qrules", quasilyterules.Bundle)
}
func txDeferRollback(m dsl.Matcher) {
// Common pattern for long-living transactions:
// tx, err := db.Begin()
// if err != nil {
// return err
// }
// defer tx.Rollback()
//
// ... code which uses database in transaction
//
// err := tx.Commit()
// if err != nil {
// return err
// }
m.Match(
`$tx, $err := $db.BeginRw($ctx); $chk; $rollback`,
`$tx, $err = $db.BeginRw($ctx); $chk; $rollback`,
`$tx, $err := $db.Begin($ctx); $chk; $rollback`,
`$tx, $err = $db.Begin($ctx); $chk; $rollback`,
`$tx, $err := $db.BeginRo($ctx); $chk; $rollback`,
`$tx, $err = $db.BeginRo($ctx); $chk; $rollback`,
).
Where(!m["rollback"].Text.Matches(`defer .*\.Rollback()`)).
//At(m["rollback"]).
Report(`Add "defer $tx.Rollback()" right after transaction creation error check.
If you are in the loop - consider use "$db.View" or "$db.Update" or extract whole transaction to function.
Without rollback in defer - app can deadlock on error or panic.
Rules are in ./rules.go file.
`)
}
func closeCollector(m dsl.Matcher) {
m.Match(`$c := etl.NewCollector($*_); $close`).
Where(!m["close"].Text.Matches(`defer .*\.Close()`)).
Report(`Add "defer $c.Close()" right after collector creation`)
}
func closeLockedDir(m dsl.Matcher) {
m.Match(`$c := dir.OpenRw($*_); $close`).
Where(!m["close"].Text.Matches(`defer .*\.Close()`)).
Report(`Add "defer $c.Close()" after locked.OpenDir`)
}
func passValuesByContext(m dsl.Matcher) {
m.Match(`ctx.WithValue($*_)`).Report(`Don't pass app-level parameters by context, pass them as-is or as typed objects`)
}
func mismatchingUnlock(m dsl.Matcher) {
// By default, an entire match position is used as a location.
// This can be changed by the At() method that binds the location
// to the provided named submatch.
//
// In the rules below text editor would get mismatching method
// name locations:
//
// defer mu.RUnlock()
// ^^^^^^^
m.Match(`$mu.Lock(); defer $mu.$unlock()`).
Where(m["unlock"].Text == "RUnlock").
At(m["unlock"]).
Report(`maybe $mu.Unlock() was intended?
Rules are in ./rules.go file.`)
m.Match(`$mu.RLock(); defer $mu.$unlock()`).
Where(m["unlock"].Text == "Unlock").
At(m["unlock"]).
Report(`maybe $mu.RUnlock() was intended?
Rules are in ./rules.go file.`)
}

113
src/agent/agent.go Normal file
View File

@ -0,0 +1,113 @@
package agent
import (
"agent/src/common"
"agent/src/utils/log"
"agent/src/workflow"
"context"
"fmt"
"net/http"
"sync"
)
const AgentWorkers = 4
type Agent struct {
ctx context.Context
apicheck *keyChecker
clients *common.ThreadSafeMap[*client, struct{}]
docPath string
}
var once sync.Once
func NewAgent(ctx context.Context, docPath string) *Agent {
return &Agent{
ctx : ctx,
clients: new(common.ThreadSafeMap[*client, struct{}]).Init(nil, false),
docPath: docPath,
}
}
func (a *Agent) Start(port uint64, crtDir *string) {
workflow.InitTaskPool(a.ctx)
go func(){
sm := http.NewServeMux()
// sm.HandleFunc("/simpletest2025", a.serveTestPage)
sm.HandleFunc("/ws", a.serveWs)
sm.HandleFunc("/doc", a.serveDoc)
sm.HandleFunc("/assets/", a.serveAssets)
addr := fmt.Sprintf(":%d", port)
log.Info("[agent] start websocket server", "addr", addr)
var err error
if crtDir == nil {
err = http.ListenAndServe(addr, sm)
} else {
err = http.ListenAndServeTLS(addr, fmt.Sprintf("%s/server.crt", *crtDir), fmt.Sprintf("%s/server.key", *crtDir), sm)
}
if err != nil {
log.Crit("ListenAndServe: ", "err", err)
}
}()
}
func (a *Agent) serveTestPage(w http.ResponseWriter, r *http.Request) {
log.Info("serveHome", "url", r.URL)
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
http.ServeFile(w, r, "home.html")
}
func (a *Agent) serveDoc(w http.ResponseWriter, r *http.Request) {
log.Info("serveHome", "url", r.URL)
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
http.ServeFile(w, r, a.docPath+"/agent服务协议.html")
}
func (a *Agent) serveAssets(w http.ResponseWriter, r *http.Request) {
log.Info("serveAssets", "url", r.URL)
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
fmt.Println(r.URL.Path[1:])
http.ServeFile(w, r, a.docPath+"/" +r.URL.Path[1:])
}
func (a *Agent) serveWs(w http.ResponseWriter, r *http.Request) {
// TODO IP连接数限制
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Error("[agent] serveWs", "err", err)
return
}
log.Info("[agent] new connection", "remote", conn.RemoteAddr())
c := newClient(conn, a)
// check api key
if a.apicheck.check(c, r) {
c.run()
a.clients.Set(c, struct{}{})
} else {
go func() {
c.SendText("HTTP/1.1 401 Unauthorized\r\n\r\n")
conn.Close()
}()
}
}
func (a *Agent) removeClient(c *client) {
a.clients.Delete(c)
}

36
src/agent/agent_test.go Normal file
View File

@ -0,0 +1,36 @@
package agent
import (
"agent/src/prompt"
"context"
"fmt"
"strings"
"testing"
)
func TestAgent(t *testing.T) {
ctx := context.Background()
a := NewAgent(ctx, "../../docs")
crt := "../../config"
a.Start(8080, &crt)
<-make(chan bool)
}
func Test001(t *testing.T) {
s := "\u003cthink\u003e\nOkay, so I'm trying to understand this code that someone provided. It's about creating a snake game using Python and Tkinter. The selected portion is the __init__ method of the Food class. Let me break it down step by step.\n\nFirst, I see that the Food class has an __init__ method. That means when a new instance of Food is created, this method runs automatically. Inside this method, there are some variables being set: x and y. They're using random.randint to generate these values. \n\nLooking at how x is calculated: it's random.randint(0, (WIDTH / SPACE_SIZE) - 1) multiplied by SPACE_SIZE. Similarly for y. I think WIDTH and HEIGHT are the dimensions of the game window, and SPACE_SIZE is probably the size of each grid block or the size of the food item. So, dividing WIDTH by SPACE_SIZE gives the number of possible positions along the width. Subtracting 1 ensures that the random number doesn't go out of bounds when multiplied back by SPACE_SIZE.\n\nSo x and y are being set to a position within the game window, in increments of SPACE_SIZE. That makes sense because it keeps the food aligned with the grid where the snake moves.\n\nNext, self.coordinates is assigned as a list [x, y]. This probably holds the current position of the food so that other parts of the code can check if the snake has eaten it.\n\nThen there's a call to canvas.create_oval. The parameters are x, y for the top-left corner and x + SPACE_SIZE, y + SPACE_SIZE for the bottom-right corner. The fill color is FOOD, which I assume is defined earlier as a constant. The tag \"food\" might be used later to identify this object on the canvas.\n\nPutting it all together, when a Food object is created, it randomly places an oval-shaped food item somewhere on the game canvas. This position is stored so that the snake can check for collisions or eating events in subsequent turns.\n\nI'm a bit confused about why they use create_oval instead of another shape, but I guess it's just a design choice. Also, I wonder how the food respawns when eaten. Maybe there's another part of the code that deletes the old food and creates a new instance of Food, which would call this __init__ method again.\n\nAnother thing to note is that the coordinates are stored as [x, y], which will be used in collision detection with the snake's head. If the snake's head position matches the food's coordinates, then the snake grows, and the score increases.\n\nI think I've got a good grasp of what this code does now. It initializes the food at a random valid position on the canvas and sets up its visual representation.\n\u003c/think\u003e\n\nThe selected code is from the `__init__` method of the `Food` class in a Python-based snake game. Here's an explanation of its logic:\n\n1. **Random Position Calculation**:\n - The `x` and `y` coordinates for the food are determined using `random.randint()`.\n - `(WIDTH / SPACE_SIZE) - 1` calculates the maximum index along the width, ensuring the food stays within bounds.\n - Multiplying by `SPACE_SIZE` converts this index to pixel coordinates.\n\n2. **Storing Coordinates**:\n - The food's position is stored in `self.coordinates` as `[x, y]`, allowing other parts of the code to check for collisions with the snake.\n\n3. **Visual Representation**:\n - An oval is drawn on the canvas using `create_oval()`, positioned at `(x, y)` with dimensions based on `SPACE_SIZE`.\n - The color is set to `FOOD` (defined earlier), and tagged as \"food\" for easy reference.\n\nThis method initializes the food's position and appearance each time a new `Food` object is created."
fmt.Println(prompt.RemoveThinkPart(s))
}
func Test002(t *testing.T) {
prompt_docstring := strings.Replace(
`a
file: {%s}
"""
{%s}
"""
`,
"\"\"\"", "```", 2)
fmt.Println(prompt_docstring)
// fmt.Println(fmt.Sprintf(prompt_docstring, "a", "b"))
}

21
src/agent/apikey.go Normal file
View File

@ -0,0 +1,21 @@
package agent
import (
"net/http"
"strings"
)
type keyChecker struct {}
func (a *keyChecker) check(c *client, r *http.Request) bool {
if c.authenticated {
return true
}
if apikey, ok := r.Header["X-Api-Key"]; ok {
if len(apikey) > 0 && strings.HasPrefix(apikey[0], "simpletest2025_") {
c.setApiKey(apikey[0])
return true
}
}
return false
}

236
src/agent/client.go Normal file
View File

@ -0,0 +1,236 @@
package agent
import (
"context"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"agent/src/message"
"agent/src/utils/log"
"agent/src/workflow"
"github.com/gorilla/websocket"
"golang.org/x/time/rate"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
const (
// Time allowed to write a message to the peer.
writeWait = 15 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 3 * 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 10*1024*1024
)
type client struct {
ctx context.Context
conn *websocket.Conn
outMsg chan []byte
authenticated bool
apikey string
agent *Agent
fnCancel context.CancelFunc
rate *rate.Limiter
wantResponseId uint64
wantResponse chan []byte
lock sync.Mutex
}
func newClient(conn *websocket.Conn, a *Agent) *client {
ctx, fnCancel := context.WithCancel(a.ctx)
return &client{
ctx : ctx,
conn : conn,
outMsg: make(chan []byte, 32),
agent : a,
fnCancel: fnCancel,
rate: rate.NewLimiter(5, 1),
}
}
func (c *client) setApiKey(apikey string) {
c.apikey = apikey
c.authenticated = true
}
func (c *client) run() {
go c.readPump()
go c.writePump()
}
func (c *client) readPump() {
defer c.Close()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
done := c.ctx.Done()
for {
select {
case <-done:
return
default:
}
_, msg, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Error(fmt.Sprintf("error: %v", err))
}
break
}
if c.rate.Allow() {
workflow.Handle(c, msg)
} else {
c.WriteJson(&message.ReponseError{
Error: "maximum 5 requests per second",
}, true)
}
}
}
func (c *client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
defer c.Close()
done := c.ctx.Done()
for {
select {
case message, ok := <-c.outMsg:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
case <-done:
return
}
}
}
func (c *client) Close() {
c.fnCancel()
c.agent.removeClient(c)
c.conn.Close()
}
func (c *client) WriteJson(data interface{}, block bool) error {
b, err := json.MarshalIndent(data, "", "")
if err != nil {
return err
}
if block {
select{
case <-c.ctx.Done():
return errors.New("connection closed")
case c.outMsg <- b:
return nil
}
} else {
select {
case <-c.ctx.Done():
return errors.New("connection closed")
case c.outMsg <- b:
return nil
default:
go func(){
select{
case <-c.ctx.Done():
case c.outMsg <- b:
}
}()
}
}
return nil
}
func (c *client) WriteText(msg string, block bool) error {
if block {
select {
case <-c.ctx.Done():
return errors.New("connection closed")
case c.outMsg <- []byte(msg):
return nil
}
} else {
select {
case c.outMsg <- []byte(msg):
default:
}
}
return nil
}
// send string, block util flush
func (c *client) SendText(msg string) error {
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return err
}
_, err = w.Write([]byte(msg))
if err != nil {
return err
}
err = w.Close()
return err
}
func (c *client) GetCtx() context.Context {
return c.ctx
}
func (c *client) GetChannel() chan<- []byte {
return c.outMsg
}
func (c *client) WantResponse(requestId uint64, ch chan []byte) {
c.lock.Lock()
c.wantResponseId, c.wantResponse = requestId, ch
c.lock.Unlock()
close(c.wantResponse)
}
func (c *client) NewWantResponse(requestId uint64, msg []byte) error {
c.lock.Lock()
defer c.lock.Unlock()
if requestId != c.wantResponseId || c.wantResponse == nil {
return errors.New("unexpected msg")
}
c.wantResponse <- msg
return nil
}

33
src/common/id.go Normal file
View File

@ -0,0 +1,33 @@
package common
import (
"sync"
"sync/atomic"
"time"
)
var (
taskId = new(atomic.Uint64)
requestId uint64
requestId_lock = new(sync.Mutex)
)
func init() {
taskId.Store(uint64(time.Now().Unix()))
}
func NewTaskId() uint64 {
return taskId.Add(1)
}
func NewRequestId() uint64 {
requestId_lock.Lock()
defer requestId_lock.Unlock()
now := uint64(time.Now().Unix())
if now > requestId {
requestId = now
} else {
requestId++
}
return requestId
}

152
src/common/queue.go Normal file
View File

@ -0,0 +1,152 @@
package common
import (
"container/list"
"sync"
)
type Queue[T any] struct {
l *list.List
lock *sync.RWMutex
}
func NewQueue[T any](threadSafe bool) *Queue[T] {
qu := &Queue[T]{
l: list.New(),
}
if threadSafe {
qu.lock = new(sync.RWMutex)
}
return qu
}
func (qu *Queue[T]) In(d T) {
if qu.lock != nil {
qu.lock.Lock()
defer qu.lock.Unlock()
}
qu.l.PushBack(d)
}
func (qu *Queue[T]) Out() T {
if qu.lock != nil {
qu.lock.Lock()
defer qu.lock.Unlock()
}
d := qu.l.Front()
if d != nil {
qu.l.Remove(d)
return d.Value.(T)
}
var t T
return t
}
func (qu *Queue[T]) Len() int {
if qu.lock != nil {
qu.lock.RLock()
defer qu.lock.RUnlock()
}
return qu.l.Len()
}
func (qu *Queue[T]) PeakFront() *T {
if qu.lock != nil {
qu.lock.RLock()
defer qu.lock.RUnlock()
}
e := qu.l.Front()
if e == nil {
return nil
}
d := e.Value.(T)
return &d
}
func (qu *Queue[T]) PeakBack() *T {
if qu.lock != nil {
qu.lock.RLock()
defer qu.lock.RUnlock()
}
e := qu.l.Front()
if e == nil {
return nil
}
d := e.Value.(T)
return &d
}
/*
fn return false to stop
*/
func (qu *Queue[T]) Traverse(fromHead bool, fn func(index int, d T) (goToNext bool)) {
if qu.lock != nil {
qu.lock.RLock()
defer qu.lock.RUnlock()
}
if fromHead {
var i int
for cur := qu.l.Front(); cur != nil; cur = cur.Next() {
if !fn(i, cur.Value.(T)) {
return
}
i++
}
} else {
var i = qu.l.Len()
for cur := qu.l.Back(); cur != nil; cur = cur.Prev() {
i--
if !fn(i, cur.Value.(T)) {
return
}
}
}
}
/*
fn return true to delete
*/
func (qu *Queue[T]) TraverseDelete(fromHead bool, fn func(index int, d T) (delete bool)) {
if qu.lock != nil {
qu.lock.Lock()
defer qu.lock.Unlock()
}
if fromHead {
var i int
for cur := qu.l.Front(); cur != nil; {
if fn(i, cur.Value.(T)) {
next := cur.Next()
qu.l.Remove(cur)
cur = next
} else {
cur = cur.Next()
}
i++
}
} else {
var i = qu.l.Len()
for cur := qu.l.Back(); cur != nil; {
i--
if fn(i, cur.Value.(T)) {
next := cur.Prev()
qu.l.Remove(cur)
cur = next
} else {
cur = cur.Prev()
}
}
}
}
/*
clear all element
*/
func (qu *Queue[T]) Reset() {
if qu.lock != nil {
qu.lock.Lock()
defer qu.lock.Unlock()
}
qu.l.Init()
}

48
src/common/taskQueue.go Normal file
View File

@ -0,0 +1,48 @@
package common
import "context"
// threadsafe
type TaskQueue[T any] struct {
Queue[*T]
maxsize int
hasNew chan struct{}
}
func NewTaskQueue[T any](maxsize int, threadsafe bool) *TaskQueue[T] {
return &TaskQueue[T]{
Queue : *NewQueue[*T](threadsafe),
maxsize: maxsize,
hasNew : make(chan struct{}, 1),
}
}
func (q *TaskQueue[T]) Push(t *T) (ok bool) {
if q.Len() >= q.maxsize {
return false
} else {
q.In(t)
select {
case q.hasNew <- struct{}{}:
default:
}
return true
}
}
func (q *TaskQueue[T]) Pop() *T {
return q.Out()
}
func (q *TaskQueue[T]) WaitForNew(ctx context.Context) *T {
for {
t := q.Out()
if t != nil {
return t
}
select {
case <-ctx.Done(): return nil
case <-q.hasNew:
}
}
}

View File

@ -0,0 +1,142 @@
package common
import (
"maps"
"sync"
"sync/atomic"
)
//********************************************************************************************************************************
// share lock (START)
//********************************************************************************************************************************
type ThreadSafeMap[K comparable, V any] struct {
M map[K]V
lock sync.RWMutex
}
/*
mp 可以为nil
*/
func (m *ThreadSafeMap[K,V]) Init(mp map[K]V, clone bool) *ThreadSafeMap[K,V] {
if mp == nil {
m.M = make(map[K]V)
} else {
if clone {
m.M = maps.Clone(mp)
} else {
m.M = mp
}
}
return m
}
func (m *ThreadSafeMap[K,V]) Get(k K) (v V, ok bool) {
m.lock.RLock()
v, ok = m.M[k]
m.lock.RUnlock()
return
}
func (m *ThreadSafeMap[K,V]) Contain(k K) (ok bool) {
m.lock.RLock()
_, ok = m.M[k]
m.lock.RUnlock()
return
}
func (m *ThreadSafeMap[K,V]) Set(k K, v V) {
m.lock.Lock()
m.M[k] = v
m.lock.Unlock()
}
func (m *ThreadSafeMap[K,V]) Do(fn func(map[K]V)) {
m.lock.Lock()
fn(m.M)
m.lock.Unlock()
}
// only replace exist
func (m *ThreadSafeMap[K,V]) ReplaceIfExist(k K, v V) V {
m.lock.Lock()
old := m.M[k]
m.M[k] = v
m.lock.Unlock()
return old
}
func (m *ThreadSafeMap[K,V]) Delete(k K) {
m.lock.Lock()
delete(m.M, k)
m.lock.Unlock()
}
func (m *ThreadSafeMap[K,V]) Size() int {
m.lock.RLock()
ln := len(m.M)
m.lock.RUnlock()
return ln
}
func (m *ThreadSafeMap[K,V]) Rest() map[K]V {
m.lock.Lock()
defer m.lock.Unlock()
old := m.M
m.M = make(map[K]V)
return old
}
func (m *ThreadSafeMap[K,V]) Run(fn func(map[K]V) error) error {
m.lock.Lock()
defer m.lock.Unlock()
return fn(m.M)
}
/*
遍历 m, fn 返回 false 结束
*/
func (m *ThreadSafeMap[K,V]) Traverse(fn func(K,V) bool) {
m.lock.Lock()
defer m.lock.Unlock()
for k, v := range m.M {
if !fn(k, v) { return }
}
}
func (m *ThreadSafeMap[K,V]) Lock() { m.lock.Lock() }
func (m *ThreadSafeMap[K,V]) UnLock() { m.lock.Unlock() }
//********************************************************************************************************************************
// share lock (END)
//********************************************************************************************************************************
//********************************************************************************************************************************
// atomic (START)
//********************************************************************************************************************************
type AtomicMap[K comparable, V any] map[K]*atomic.Pointer[V]
func NewAtomicMap[K comparable, V any]() AtomicMap[K, V] {
return make(map[K]*atomic.Pointer[V])
}
func (m AtomicMap[K, V]) Add(k K, ptrV *V) {
if a, ok := m[k]; ok {
a.Store(ptrV)
} else {
a = new(atomic.Pointer[V])
a.Store(ptrV)
m[k] = a
}
}
func (m AtomicMap[K, V]) Get(k K) *V {
if a, ok := m[k]; ok {
return a.Load()
} else {
return nil
}
}
//********************************************************************************************************************************
// atomic (END)
//********************************************************************************************************************************

Some files were not shown because too many files have changed in this diff Show More