diff --git a/config/config.yaml b/config/config.yaml
index 29ccb79ce..04afd9479 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -618,57 +618,16 @@ agent:
web_search:
# 可用搜索引擎列表
providers:
- - id: "kuaisou"
- name: "快搜"
- free: false
- requires_api_key: true
- description: "高性能实时网页搜索"
- api_url: "https://platform.kuaisou.com/api/v1/search"
- - id: "google"
- name: "Google"
+ - id: "duckduckgo"
+ name: "DuckDuckGo"
free: true
requires_api_key: false
- description: "Google搜索(免费)"
- api_url: "https://www.googleapis.com/customsearch/v1"
- - id: "baidu"
- name: "百度"
- free: true
- requires_api_key: false
- description: "百度搜索(免费)"
- - id: "bing"
- name: "Bing"
- free: true
- requires_api_key: false
- description: "Bing搜索(免费)"
- - id: "bocha"
- name: "博查"
- free: false
- requires_api_key: true
- description: "专为AI应用设计的搜索API"
- - id: "zhipu"
- name: "智谱"
- free: false
- requires_api_key: true
- description: "智谱搜索API"
- - id: "tavily"
- name: "Tavily"
- free: false
- requires_api_key: true
- description: "Tavily AI搜索API"
- - id: "searxng"
- name: "Searxng"
- free: true
- requires_api_key: false
- description: "Searxng开源元搜索引擎"
- - id: "exa"
- name: "Exa"
- free: false
- requires_api_key: true
- description: "Exa AI搜索API"
+ description: "DuckDuckGo API"
+
# 默认配置
default:
- provider: "kuaisou"
+ provider: "duckduckgo"
max_results: 5
include_date: true
compression_method: "none"
diff --git a/frontend/src/views/chat/components/AgentStreamDisplay.vue b/frontend/src/views/chat/components/AgentStreamDisplay.vue
index 9e305bda5..3d78a4b38 100644
--- a/frontend/src/views/chat/components/AgentStreamDisplay.vue
+++ b/frontend/src/views/chat/components/AgentStreamDisplay.vue
@@ -44,7 +44,7 @@
-
+
{{ provider.name }}
-
- {{ provider.description }}
-
diff --git a/go.mod b/go.mod
index ce23823c2..4c1c65308 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.24.0
toolchain go1.24.2
require (
+ github.com/PuerkitoBio/goquery v1.10.3
github.com/elastic/go-elasticsearch/v7 v7.17.10
github.com/elastic/go-elasticsearch/v8 v8.18.0
github.com/gin-contrib/cors v1.7.5
@@ -45,6 +46,7 @@ require (
require (
github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
diff --git a/go.sum b/go.sum
index b810460ff..a2a66eafa 100644
--- a/go.sum
+++ b/go.sum
@@ -4,9 +4,13 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
+github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
+github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
@@ -103,6 +107,7 @@ github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg6
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -285,6 +290,7 @@ github.com/yanyiwu/gojieba v1.4.5 h1:VyZogGtdFSnJbACHvDRvDreXPPVPCg8axKFUdblU/JI
github.com/yanyiwu/gojieba v1.4.5/go.mod h1:JUq4DddFVGdHXJHxxepxRmhrKlDpaBxR8O28v6fKYLY=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
@@ -315,22 +321,85 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
diff --git a/internal/application/repository/knowledgebase.go b/internal/application/repository/knowledgebase.go
index efd7ca535..749c208ae 100644
--- a/internal/application/repository/knowledgebase.go
+++ b/internal/application/repository/knowledgebase.go
@@ -52,7 +52,7 @@ func (r *knowledgeBaseRepository) ListKnowledgeBasesByTenantID(
ctx context.Context, tenantID uint,
) ([]*types.KnowledgeBase, error) {
var kbs []*types.KnowledgeBase
- if err := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID).
+ if err := r.db.WithContext(ctx).Where("tenant_id = ? AND is_temporary = ?", tenantID, false).
Order("created_at DESC").Find(&kbs).Error; err != nil {
return nil, err
}
diff --git a/internal/application/service/chat_pipline/search.go b/internal/application/service/chat_pipline/search.go
index 69fa0c2d2..26457eb39 100644
--- a/internal/application/service/chat_pipline/search.go
+++ b/internal/application/service/chat_pipline/search.go
@@ -2,6 +2,7 @@ package chatpipline
import (
"context"
+ "encoding/json"
"fmt"
"strings"
"sync"
@@ -11,30 +12,37 @@ import (
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
+ "github.com/redis/go-redis/v9"
)
// PluginSearch implements search functionality for chat pipeline
type PluginSearch struct {
knowledgeBaseService interfaces.KnowledgeBaseService
+ knowledgeService interfaces.KnowledgeService
modelService interfaces.ModelService
config *config.Config
webSearchService interfaces.WebSearchService
tenantService interfaces.TenantService
+ redisClient *redis.Client
}
func NewPluginSearch(eventManager *EventManager,
knowledgeBaseService interfaces.KnowledgeBaseService,
+ knowledgeService interfaces.KnowledgeService,
modelService interfaces.ModelService,
config *config.Config,
webSearchService interfaces.WebSearchService,
tenantService interfaces.TenantService,
+ redisClient *redis.Client,
) *PluginSearch {
res := &PluginSearch{
knowledgeBaseService: knowledgeBaseService,
+ knowledgeService: knowledgeService,
modelService: modelService,
config: config,
webSearchService: webSearchService,
tenantService: tenantService,
+ redisClient: redisClient,
}
eventManager.Register(res)
return res
@@ -62,44 +70,37 @@ func (p *PluginSearch) OnEvent(ctx context.Context,
return ErrSearch.WithError(nil)
}
+ // Run KB search and web search concurrently
logger.Infof(ctx, "Searching across %d knowledge base(s): %v", len(knowledgeBaseIDs), knowledgeBaseIDs)
-
- // Prepare search parameters
- searchParams := types.SearchParams{
- QueryText: strings.TrimSpace(chatManage.RewriteQuery),
- VectorThreshold: chatManage.VectorThreshold,
- KeywordThreshold: chatManage.KeywordThreshold,
- MatchCount: chatManage.EmbeddingTopK,
- }
- logger.Infof(ctx, "Search parameters: %v", searchParams)
-
- // Parallel search across multiple knowledge bases
var wg sync.WaitGroup
var mu sync.Mutex
- var allResults []*types.SearchResult
-
- for _, kbID := range knowledgeBaseIDs {
- wg.Add(1)
- go func(knowledgeBaseID string) {
- defer wg.Done()
-
- results, err := p.knowledgeBaseService.HybridSearch(ctx, knowledgeBaseID, searchParams)
- if err != nil {
- logger.Errorf(ctx, "Failed to search KB %s: %v", knowledgeBaseID, err)
- return
- }
-
- logger.Infof(ctx, "KB %s search results count: %d", knowledgeBaseID, len(results))
+ allResults := make([]*types.SearchResult, 0)
+ wg.Add(2)
+ // Goroutine 1: Knowledge base search (rewrite + processed)
+ go func() {
+ defer wg.Done()
+ kbResults := p.searchKnowledgeBases(ctx, knowledgeBaseIDs, chatManage)
+ if len(kbResults) > 0 {
mu.Lock()
- allResults = append(allResults, results...)
+ allResults = append(allResults, kbResults...)
mu.Unlock()
- }(kbID)
- }
+ }
+ }()
+
+ // Goroutine 2: Web search (if enabled)
+ go func() {
+ defer wg.Done()
+ webResults := p.searchWebIfEnabled(ctx, chatManage)
+ if len(webResults) > 0 {
+ mu.Lock()
+ allResults = append(allResults, webResults...)
+ mu.Unlock()
+ }
+ }()
wg.Wait()
- logger.Infof(ctx, "Total search results from all KBs: %d", len(allResults))
chatManage.SearchResult = allResults
// Add relevant results from chat history
@@ -109,67 +110,6 @@ func (p *PluginSearch) OnEvent(ctx context.Context,
chatManage.SearchResult = append(chatManage.SearchResult, historyResult...)
}
- // Try search with processed query if different from rewrite query
- if chatManage.RewriteQuery != chatManage.ProcessedQuery {
- searchParams.QueryText = strings.TrimSpace(chatManage.ProcessedQuery)
- logger.Infof(ctx, "Searching with processed query: %s", searchParams.QueryText)
-
- var wg2 sync.WaitGroup
- var mu2 sync.Mutex
- var processedResults []*types.SearchResult
-
- for _, kbID := range knowledgeBaseIDs {
- wg2.Add(1)
- go func(knowledgeBaseID string) {
- defer wg2.Done()
-
- results, err := p.knowledgeBaseService.HybridSearch(ctx, knowledgeBaseID, searchParams)
- if err != nil {
- logger.Errorf(ctx, "Failed to search KB %s with processed query: %v", knowledgeBaseID, err)
- return
- }
-
- logger.Infof(ctx, "KB %s processed query results count: %d", knowledgeBaseID, len(results))
-
- mu2.Lock()
- processedResults = append(processedResults, results...)
- mu2.Unlock()
- }(kbID)
- }
-
- wg2.Wait()
-
- logger.Infof(ctx, "Total processed query results from all KBs: %d", len(processedResults))
- chatManage.SearchResult = append(chatManage.SearchResult, processedResults...)
- }
-
- // Perform web search if enabled and merge results with KB search results
- if chatManage.WebSearchEnabled && p.webSearchService != nil && p.tenantService != nil && chatManage.TenantID > 0 {
- // Get tenant to retrieve web search config
- tenant, err := p.tenantService.GetTenantByID(ctx, chatManage.TenantID)
- if err != nil {
- logger.Warnf(ctx, "Failed to get tenant for web search: %v", err)
- } else if tenant != nil && tenant.WebSearchConfig != nil && tenant.WebSearchConfig.Provider != "" {
- // Perform web search in parallel with KB search (already completed)
- logger.Infof(ctx, "Performing web search with provider: %s", tenant.WebSearchConfig.Provider)
- webResults, err := p.webSearchService.Search(ctx, tenant.WebSearchConfig, chatManage.RewriteQuery)
- if err != nil {
- logger.Warnf(ctx, "Web search failed: %v", err)
- } else {
- // Convert web search results to SearchResult
- webSearchResults := convertWebSearchResults(webResults)
- logger.Infof(ctx, "Web search returned %d results", len(webSearchResults))
- // Merge web search results with KB search results
- if len(webSearchResults) > 0 {
- chatManage.SearchResult = append(chatManage.SearchResult, webSearchResults...)
- logger.Infof(ctx, "Merged web search results, total results: %d", len(chatManage.SearchResult))
- }
- }
- } else {
- logger.Warnf(ctx, "Web search enabled but no valid configuration found for tenant %d", chatManage.TenantID)
- }
- }
-
// Remove duplicate results
chatManage.SearchResult = removeDuplicateResults(chatManage.SearchResult)
@@ -216,6 +156,136 @@ func removeDuplicateResults(results []*types.SearchResult) []*types.SearchResult
return uniqueResults
}
+// searchKnowledgeBases performs KB searches for rewrite and processed queries across KB IDs
+func (p *PluginSearch) searchKnowledgeBases(ctx context.Context, knowledgeBaseIDs []string, chatManage *types.ChatManage) []*types.SearchResult {
+ // Build base params for rewrite query
+ baseParams := types.SearchParams{
+ QueryText: strings.TrimSpace(chatManage.RewriteQuery),
+ VectorThreshold: chatManage.VectorThreshold,
+ KeywordThreshold: chatManage.KeywordThreshold,
+ MatchCount: chatManage.EmbeddingTopK,
+ }
+
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ var results []*types.SearchResult
+
+ // Search with rewrite query
+ for _, kbID := range knowledgeBaseIDs {
+ wg.Add(1)
+ go func(knowledgeBaseID string) {
+ defer wg.Done()
+ res, err := p.knowledgeBaseService.HybridSearch(ctx, knowledgeBaseID, baseParams)
+ if err != nil {
+ logger.Errorf(ctx, "Failed to search KB %s: %v", knowledgeBaseID, err)
+ return
+ }
+ logger.Infof(ctx, "KB %s search results count: %d", knowledgeBaseID, len(res))
+ mu.Lock()
+ results = append(results, res...)
+ mu.Unlock()
+ }(kbID)
+ }
+
+ wg.Wait()
+
+ // If processed query differs, search again
+ if chatManage.RewriteQuery != chatManage.ProcessedQuery {
+ paramsProcessed := baseParams
+ paramsProcessed.QueryText = strings.TrimSpace(chatManage.ProcessedQuery)
+ logger.Infof(ctx, "Searching with processed query: %s", paramsProcessed.QueryText)
+
+ wg = sync.WaitGroup{}
+ for _, kbID := range knowledgeBaseIDs {
+ wg.Add(1)
+ go func(knowledgeBaseID string) {
+ defer wg.Done()
+ res, err := p.knowledgeBaseService.HybridSearch(ctx, knowledgeBaseID, paramsProcessed)
+ if err != nil {
+ logger.Errorf(ctx, "Failed to search KB %s with processed query: %v", knowledgeBaseID, err)
+ return
+ }
+ logger.Infof(ctx, "KB %s processed query results count: %d", knowledgeBaseID, len(res))
+ mu.Lock()
+ results = append(results, res...)
+ mu.Unlock()
+ }(kbID)
+ }
+ wg.Wait()
+ }
+
+ logger.Infof(ctx, "Total KB results (rewrite + processed): %d", len(results))
+ return results
+}
+
+// searchWebIfEnabled executes web search when enabled and returns converted results
+func (p *PluginSearch) searchWebIfEnabled(ctx context.Context, chatManage *types.ChatManage) []*types.SearchResult {
+ if !(chatManage.WebSearchEnabled && p.webSearchService != nil && p.tenantService != nil && chatManage.TenantID > 0) {
+ return nil
+ }
+ tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
+ if tenant == nil || tenant.WebSearchConfig == nil || tenant.WebSearchConfig.Provider == "" {
+ logger.Warnf(ctx, "Web search enabled but no valid configuration found for tenant %d", chatManage.TenantID)
+ return nil
+ }
+
+ logger.Infof(ctx, "Performing web search with provider: %s", tenant.WebSearchConfig.Provider)
+ webResults, err := p.webSearchService.Search(ctx, tenant.WebSearchConfig, chatManage.RewriteQuery)
+ if err != nil {
+ logger.Warnf(ctx, "Web search failed: %v", err)
+ return nil
+ }
+ // Build questions (rewrite + processed if different)
+ questions := []string{strings.TrimSpace(chatManage.RewriteQuery)}
+ if chatManage.ProcessedQuery != "" && chatManage.ProcessedQuery != chatManage.RewriteQuery {
+ questions = append(questions, strings.TrimSpace(chatManage.ProcessedQuery))
+ }
+ // Load session-scoped temp KB state from Redis
+ var tempKBID string
+ seen := map[string]bool{}
+ ids := []string{}
+ stateKey := fmt.Sprintf("tempkb:%s", chatManage.SessionID)
+ if raw, getErr := p.redisClient.Get(ctx, stateKey).Bytes(); getErr == nil && len(raw) > 0 {
+ var state struct {
+ KBID string `json:"kbID"`
+ KnowledgeIDs []string `json:"knowledgeIDs"`
+ SeenURLs map[string]bool `json:"seenURLs"`
+ }
+ if err := json.Unmarshal(raw, &state); err == nil {
+ tempKBID = state.KBID
+ ids = state.KnowledgeIDs
+ if state.SeenURLs != nil {
+ seen = state.SeenURLs
+ }
+ }
+ }
+ compressed, kbID, newSeen, newIDs, err := p.webSearchService.CompressWithRAG(
+ ctx, chatManage.SessionID, tempKBID, questions, webResults, tenant.WebSearchConfig,
+ p.knowledgeBaseService, p.knowledgeService, seen, ids,
+ )
+ if err != nil {
+ logger.Warnf(ctx, "RAG compression failed, falling back to raw: %v", err)
+ } else {
+ webResults = compressed
+ // Persist temp KB state back into Redis
+ state := struct {
+ KBID string `json:"kbID"`
+ KnowledgeIDs []string `json:"knowledgeIDs"`
+ SeenURLs map[string]bool `json:"seenURLs"`
+ }{
+ KBID: kbID,
+ KnowledgeIDs: newIDs,
+ SeenURLs: newSeen,
+ }
+ if b, mErr := json.Marshal(state); mErr == nil {
+ _ = p.redisClient.Set(ctx, stateKey, b, 0).Err()
+ }
+ }
+ res := convertWebSearchResults(webResults)
+ logger.Infof(ctx, "Web search returned %d results", len(res))
+ return res
+}
+
// convertWebSearchResults converts WebSearchResult to SearchResult
// This is a duplicate of the function in service/web_search.go to avoid circular imports
func convertWebSearchResults(webResults []*types.WebSearchResult) []*types.SearchResult {
diff --git a/internal/application/service/knowledge.go b/internal/application/service/knowledge.go
index 886866f06..7f756c06d 100644
--- a/internal/application/service/knowledge.go
+++ b/internal/application/service/knowledge.go
@@ -351,7 +351,26 @@ func (s *knowledgeService) CreateKnowledgeFromURL(ctx context.Context,
func (s *knowledgeService) CreateKnowledgeFromPassage(ctx context.Context,
kbID string, passage []string,
) (*types.Knowledge, error) {
- logger.Info(ctx, "Start creating knowledge from passage")
+ return s.createKnowledgeFromPassageInternal(ctx, kbID, passage, false)
+}
+
+// CreateKnowledgeFromPassageSync creates a knowledge entry from text passages and waits for indexing to complete.
+func (s *knowledgeService) CreateKnowledgeFromPassageSync(ctx context.Context,
+ kbID string, passage []string,
+) (*types.Knowledge, error) {
+ return s.createKnowledgeFromPassageInternal(ctx, kbID, passage, true)
+}
+
+// createKnowledgeFromPassageInternal consolidates the common logic for creating knowledge from passages.
+// When syncMode is true, chunk processing is performed synchronously; otherwise, it's processed asynchronously.
+func (s *knowledgeService) createKnowledgeFromPassageInternal(ctx context.Context,
+ kbID string, passage []string, syncMode bool,
+) (*types.Knowledge, error) {
+ if syncMode {
+ logger.Info(ctx, "Start creating knowledge from passage (sync)")
+ } else {
+ logger.Info(ctx, "Start creating knowledge from passage")
+ }
logger.Infof(ctx, "Knowledge base ID: %s, passage count: %d", kbID, len(passage))
// 验证段落内容安全性
@@ -374,7 +393,11 @@ func (s *knowledgeService) CreateKnowledgeFromPassage(ctx context.Context,
}
// Create knowledge record
- logger.Info(ctx, "Creating knowledge record")
+ if syncMode {
+ logger.Info(ctx, "Creating knowledge record (sync)")
+ } else {
+ logger.Info(ctx, "Creating knowledge record")
+ }
knowledge := &types.Knowledge{
ID: uuid.New().String(),
TenantID: ctx.Value(types.TenantIDContextKey).(uint),
@@ -394,11 +417,16 @@ func (s *knowledgeService) CreateKnowledgeFromPassage(ctx context.Context,
return nil, err
}
- // Process passages asynchronously
- logger.Info(ctx, "Starting asynchronous passage processing")
- go s.processDocumentFromPassage(ctx, kb, knowledge, safePassages)
-
- logger.Infof(ctx, "Knowledge from passage created successfully, ID: %s", knowledge.ID)
+ // Process passages
+ if syncMode {
+ logger.Info(ctx, "Processing passage synchronously")
+ s.processDocumentFromPassage(ctx, kb, knowledge, safePassages)
+ logger.Infof(ctx, "Knowledge from passage created successfully (sync), ID: %s", knowledge.ID)
+ } else {
+ logger.Info(ctx, "Starting asynchronous passage processing")
+ go s.processDocumentFromPassage(ctx, kb, knowledge, safePassages)
+ logger.Infof(ctx, "Knowledge from passage created successfully, ID: %s", knowledge.ID)
+ }
return knowledge, nil
}
diff --git a/internal/application/service/session.go b/internal/application/service/session.go
index 91d09a8cb..3f8c8f23d 100644
--- a/internal/application/service/session.go
+++ b/internal/application/service/session.go
@@ -2,6 +2,7 @@ package service
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"strings"
@@ -16,6 +17,7 @@ import (
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
"github.com/google/uuid"
+ "github.com/redis/go-redis/v9"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)
@@ -36,6 +38,8 @@ type sessionService struct {
eventManager *chatpipline.EventManager // Event manager for chat pipeline
agentService interfaces.AgentService // Service for agent operations
sessionStorage llmcontext.ContextStorage // Session storage
+ knowledgeService interfaces.KnowledgeService // Service for knowledge operations
+ redisClient *redis.Client // Redis client for temp KB state
}
// NewSessionService creates a new session service instance with all required dependencies
@@ -43,22 +47,26 @@ func NewSessionService(cfg *config.Config,
sessionRepo interfaces.SessionRepository,
messageRepo interfaces.MessageRepository,
knowledgeBaseService interfaces.KnowledgeBaseService,
+ knowledgeService interfaces.KnowledgeService,
modelService interfaces.ModelService,
tenantService interfaces.TenantService,
eventManager *chatpipline.EventManager,
agentService interfaces.AgentService,
sessionStorage llmcontext.ContextStorage,
+ redisClient *redis.Client,
) interfaces.SessionService {
return &sessionService{
cfg: cfg,
sessionRepo: sessionRepo,
messageRepo: messageRepo,
knowledgeBaseService: knowledgeBaseService,
+ knowledgeService: knowledgeService,
modelService: modelService,
tenantService: tenantService,
eventManager: eventManager,
agentService: agentService,
sessionStorage: sessionStorage,
+ redisClient: redisClient,
}
}
@@ -202,6 +210,30 @@ func (s *sessionService) DeleteSession(ctx context.Context, id string) error {
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
logger.Infof(ctx, "Deleting session, ID: %s, tenant ID: %d", id, tenantID)
+ // Cleanup temporary KB stored in Redis for this session
+ if s.redisClient != nil {
+ stateKey := fmt.Sprintf("tempkb:%s", id)
+ if raw, getErr := s.redisClient.Get(ctx, stateKey).Bytes(); getErr == nil && len(raw) > 0 {
+ var state struct {
+ KBID string `json:"kbID"`
+ KnowledgeIDs []string `json:"knowledgeIDs"`
+ SeenURLs map[string]bool `json:"seenURLs"`
+ }
+ if err := json.Unmarshal(raw, &state); err == nil && strings.TrimSpace(state.KBID) != "" {
+ logger.Infof(ctx, "Cleaning temporary KB for session %s: %s", id, state.KBID)
+ for _, kid := range state.KnowledgeIDs {
+ if delErr := s.knowledgeService.DeleteKnowledge(ctx, kid); delErr != nil {
+ logger.Warnf(ctx, "Failed to delete temp knowledge %s: %v", kid, delErr)
+ }
+ }
+ if delErr := s.knowledgeBaseService.DeleteKnowledgeBase(ctx, state.KBID); delErr != nil {
+ logger.Warnf(ctx, "Failed to delete temp knowledge base %s: %v", state.KBID, delErr)
+ }
+ _ = s.redisClient.Del(ctx, stateKey).Err()
+ }
+ }
+ }
+
// Delete session from repository
err := s.sessionRepo.Delete(ctx, tenantID, id)
if err != nil {
diff --git a/internal/application/service/web_search.go b/internal/application/service/web_search.go
index 2c1b1495b..d53e79fe3 100644
--- a/internal/application/service/web_search.go
+++ b/internal/application/service/web_search.go
@@ -7,23 +7,238 @@ import (
"strings"
"time"
+ "github.com/Tencent/WeKnora/internal/application/service/web_search"
"github.com/Tencent/WeKnora/internal/config"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
+ "github.com/Tencent/WeKnora/internal/types/interfaces"
)
-// WebSearchProvider defines the interface for web search providers
-type WebSearchProvider interface {
- Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error)
- Name() string
-}
-
// WebSearchService provides web search functionality
type WebSearchService struct {
- providers map[string]WebSearchProvider
+ providers map[string]interfaces.WebSearchProvider
config *config.WebSearchConfig
}
+// CompressWithRAG performs RAG-based compression using a temporary, hidden knowledge base.
+// The temporary knowledge base is deleted after use. The UI will not list it due to repo filtering.
+func (s *WebSearchService) CompressWithRAG(
+ ctx context.Context, sessionID string, tempKBID string, questions []string,
+ webSearchResults []*types.WebSearchResult, cfg *types.WebSearchConfig,
+ kbSvc interfaces.KnowledgeBaseService, knowSvc interfaces.KnowledgeService,
+ seenURLs map[string]bool, knowledgeIDs []string,
+) (compressed []*types.WebSearchResult, kbID string, newSeen map[string]bool, newIDs []string, err error) {
+ if len(webSearchResults) == 0 || len(questions) == 0 {
+ return
+ }
+ if cfg == nil {
+ return nil, tempKBID, seenURLs, knowledgeIDs, fmt.Errorf("web search config is required for RAG compression")
+ }
+ if cfg.EmbeddingModelID == "" {
+ return nil, tempKBID, seenURLs, knowledgeIDs, fmt.Errorf("embedding_model_id is required for RAG compression")
+ }
+ var createdKB *types.KnowledgeBase
+ // reuse or create temp KB
+ if strings.TrimSpace(tempKBID) != "" {
+ createdKB, err = kbSvc.GetKnowledgeBaseByID(ctx, tempKBID)
+ if err != nil {
+ logger.Warnf(ctx, "Temp KB %s not available, recreating: %v", tempKBID, err)
+ createdKB = nil
+ }
+ }
+ if createdKB == nil {
+ kb := &types.KnowledgeBase{
+ Name: fmt.Sprintf("tmp-websearch-%d", time.Now().UnixNano()),
+ Description: "Ephemeral search compression KB",
+ IsTemporary: true,
+ EmbeddingModelID: cfg.EmbeddingModelID,
+ RerankModelID: cfg.RerankModelID,
+ }
+ createdKB, err = kbSvc.CreateKnowledgeBase(ctx, kb)
+ if err != nil {
+ return nil, tempKBID, seenURLs, knowledgeIDs, fmt.Errorf("failed to create temporary knowledge base: %w", err)
+ }
+ tempKBID = createdKB.ID
+ }
+
+ // Ingest all web results as passages synchronously
+ // dedupe by URL across queries within the same temp KB for this request/session
+ if seenURLs == nil {
+ seenURLs = map[string]bool{}
+ }
+ for _, r := range webSearchResults {
+ sourceURL := r.URL
+ title := strings.TrimSpace(r.Title)
+ snippet := strings.TrimSpace(r.Snippet)
+ body := strings.TrimSpace(r.Content)
+ // skip if already ingested for this KB
+ if sourceURL != "" && seenURLs[sourceURL] {
+ continue
+ }
+ contentLines := make([]string, 0, 4)
+ contentLines = append(contentLines, fmt.Sprintf("[sourceUrl]: %s", sourceURL))
+ if title != "" {
+ contentLines = append(contentLines, title)
+ }
+ if snippet != "" {
+ contentLines = append(contentLines, snippet)
+ }
+ if body != "" {
+ contentLines = append(contentLines, body)
+ }
+ knowledge, err := knowSvc.CreateKnowledgeFromPassageSync(ctx, createdKB.ID, contentLines)
+ if err != nil {
+ logger.Warnf(ctx, "failed to ingest passage into temp KB: %v", err)
+ continue
+ }
+ if sourceURL != "" {
+ seenURLs[sourceURL] = true
+ }
+ knowledgeIDs = append(knowledgeIDs, knowledge.ID)
+ }
+
+ // Retrieve references for questions
+ matchCount := cfg.DocumentFragments
+ if matchCount <= 0 {
+ matchCount = 3
+ }
+ var allRefs []*types.SearchResult
+ for _, q := range questions {
+ params := types.SearchParams{
+ QueryText: q,
+ VectorThreshold: 0.5,
+ KeywordThreshold: 0.5,
+ MatchCount: matchCount,
+ }
+ results, err := kbSvc.HybridSearch(ctx, tempKBID, params)
+ if err != nil {
+ logger.Warnf(ctx, "hybrid search failed for temp KB: %v", err)
+ continue
+ }
+ allRefs = append(allRefs, results...)
+ }
+
+ // Round-robin select references across the original results by source URL
+ selected := s.selectReferencesRoundRobin(webSearchResults, allRefs, matchCount*len(webSearchResults))
+ // Consolidate by URL back into the web results
+ compressedResults := s.consolidateReferencesByURL(webSearchResults, selected)
+ return compressedResults, tempKBID, seenURLs, knowledgeIDs, nil
+}
+
+// selectReferencesRoundRobin selects up to limit references, distributing fairly across source URLs.
+func (s *WebSearchService) selectReferencesRoundRobin(
+ raw []*types.WebSearchResult,
+ refs []*types.SearchResult,
+ limit int,
+) []*types.SearchResult {
+ if limit <= 0 || len(refs) == 0 {
+ return nil
+ }
+ // group refs by url marker in content
+ urlToRefs := map[string][]*types.SearchResult{}
+ for _, r := range refs {
+ url := extractSourceURLFromContent(r.Content)
+ if url == "" {
+ continue
+ }
+ urlToRefs[url] = append(urlToRefs[url], r)
+ }
+ // preserve order based on raw results
+ order := make([]string, 0, len(raw))
+ seen := map[string]bool{}
+ for _, r := range raw {
+ if r.URL != "" && !seen[r.URL] {
+ order = append(order, r.URL)
+ seen[r.URL] = true
+ }
+ }
+ var out []*types.SearchResult
+ for len(out) < limit {
+ progress := false
+ for _, url := range order {
+ if len(out) >= limit {
+ break
+ }
+ list := urlToRefs[url]
+ if len(list) == 0 {
+ continue
+ }
+ out = append(out, list[0])
+ urlToRefs[url] = list[1:]
+ progress = true
+ }
+ if !progress {
+ break
+ }
+ }
+ return out
+}
+
+// consolidateReferencesByURL merges selected references back into the original results grouped by URL.
+func (s *WebSearchService) consolidateReferencesByURL(
+ raw []*types.WebSearchResult,
+ selected []*types.SearchResult,
+) []*types.WebSearchResult {
+ if len(selected) == 0 {
+ return raw
+ }
+ agg := map[string][]string{}
+ for _, ref := range selected {
+ url := extractSourceURLFromContent(ref.Content)
+ if url == "" {
+ continue
+ }
+ // strip the first marker line to avoid duplication
+ agg[url] = append(agg[url], stripMarker(ref.Content))
+ }
+ // build outputs, preserving raw ordering and metadata
+ out := make([]*types.WebSearchResult, 0, len(raw))
+ for _, r := range raw {
+ parts := agg[r.URL]
+ if len(parts) == 0 {
+ out = append(out, r)
+ continue
+ }
+ merged := strings.Join(parts, "\n---\n")
+ out = append(out, &types.WebSearchResult{
+ Title: r.Title,
+ URL: r.URL,
+ Snippet: r.Snippet,
+ Content: merged,
+ Source: r.Source,
+ PublishedAt: r.PublishedAt,
+ })
+ }
+ return out
+}
+
+func extractSourceURLFromContent(content string) string {
+ if content == "" {
+ return ""
+ }
+ lines := strings.Split(content, "\n")
+ if len(lines) == 0 {
+ return ""
+ }
+ first := strings.TrimSpace(lines[0])
+ const prefix = "[sourceUrl]: "
+ if strings.HasPrefix(first, prefix) {
+ return strings.TrimSpace(strings.TrimPrefix(first, prefix))
+ }
+ return ""
+}
+
+func stripMarker(content string) string {
+ lines := strings.Split(content, "\n")
+ if len(lines) == 0 {
+ return content
+ }
+ if strings.HasPrefix(strings.TrimSpace(lines[0]), "[sourceUrl]: ") {
+ return strings.Join(lines[1:], "\n")
+ }
+ return content
+}
+
// Search performs web search using the specified provider
// This method implements the interface expected by PluginSearch
func (s *WebSearchService) Search(ctx context.Context, config *types.WebSearchConfig, query string) ([]*types.WebSearchResult, error) {
@@ -36,11 +251,6 @@ func (s *WebSearchService) Search(ctx context.Context, config *types.WebSearchCo
return nil, fmt.Errorf("web search provider %s is not available", config.Provider)
}
- // Set API key for providers that need it
- if config.APIKey != "" {
- s.setProviderAPIKey(config.Provider, provider, config)
- }
-
// Set timeout
timeout := time.Duration(s.config.Timeout) * time.Second
if timeout == 0 {
@@ -75,44 +285,24 @@ func NewWebSearchService(cfg *config.Config) (*WebSearchService, error) {
}
service := &WebSearchService{
- providers: make(map[string]WebSearchProvider),
+ providers: make(map[string]interfaces.WebSearchProvider),
config: cfg.WebSearch,
}
// Initialize providers based on config
for _, providerConfig := range cfg.WebSearch.Providers {
- var provider WebSearchProvider
+ var provider interfaces.WebSearchProvider
var err error
switch providerConfig.ID {
- case "kuaisou":
- provider, err = NewKuaisouProvider(providerConfig)
- case "baidu":
- provider, err = NewBaiduProvider(providerConfig)
- case "google":
- provider, err = NewGoogleProvider(providerConfig)
- case "bing":
- provider, err = NewBingProvider(providerConfig)
- case "bocha":
- provider, err = NewBochaProvider(providerConfig)
- case "zhipu":
- provider, err = NewZhipuProvider(providerConfig)
- case "tavily":
- provider, err = NewTavilyProvider(providerConfig)
- case "searxng":
- provider, err = NewSearxngProvider(providerConfig)
- case "exa":
- provider, err = NewExaProvider(providerConfig)
+ case "duckduckgo":
+ provider, err = web_search.NewDuckDuckGoProvider(providerConfig)
default:
- logger.Warnf(context.Background(), "Unknown web search provider: %s", providerConfig.ID)
- continue
+ return nil, fmt.Errorf("unknown web search provider: %s", providerConfig.ID)
}
-
if err != nil {
- logger.Warnf(context.Background(), "Failed to initialize provider %s: %v", providerConfig.ID, err)
- continue
+ return nil, fmt.Errorf("failed to initialize provider %s: %v", providerConfig.ID, err)
}
-
service.providers[providerConfig.ID] = provider
logger.Infof(context.Background(), "Initialized web search provider: %s", providerConfig.ID)
}
@@ -120,27 +310,6 @@ func NewWebSearchService(cfg *config.Config) (*WebSearchService, error) {
return service, nil
}
-// setProviderAPIKey sets the API key for a provider based on its type
-func (s *WebSearchService) setProviderAPIKey(providerID string, provider WebSearchProvider, config *types.WebSearchConfig) {
- switch p := provider.(type) {
- case *KuaisouProvider:
- p.SetAPIKey(config.APIKey)
- case *BochaProvider:
- p.SetAPIKey(config.APIKey)
- case *ZhipuProvider:
- p.SetAPIKey(config.APIKey)
- case *TavilyProvider:
- p.SetAPIKey(config.APIKey)
- case *ExaProvider:
- p.SetAPIKey(config.APIKey)
- case *GoogleProvider:
- // Google needs both API key and search engine ID
- // For now, we'll use API key as search engine ID if not provided separately
- // This can be extended later to support separate fields
- p.SetAPIKey(config.APIKey, config.APIKey) // TODO: Add search engine ID to config
- }
-}
-
// filterBlacklist filters results based on blacklist rules
func (s *WebSearchService) filterBlacklist(results []*types.WebSearchResult, blacklist []string) []*types.WebSearchResult {
if len(blacklist) == 0 {
diff --git a/internal/application/service/web_search/baidu.go b/internal/application/service/web_search/baidu.go
deleted file mode 100644
index aa527eca0..000000000
--- a/internal/application/service/web_search/baidu.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// BaiduProvider implements web search using Baidu API
-type BaiduProvider struct {
- apiURL string
-}
-
-// NewBaiduProvider creates a new Baidu provider
-func NewBaiduProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &BaiduProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *BaiduProvider) Name() string {
- return "baidu"
-}
-
-// Search performs a web search using Baidu API
-func (p *BaiduProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Baidu search API
- return nil, fmt.Errorf("baidu search provider is not yet implemented")
-}
diff --git a/internal/application/service/web_search/bing.go b/internal/application/service/web_search/bing.go
deleted file mode 100644
index bb617841a..000000000
--- a/internal/application/service/web_search/bing.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// BingProvider implements web search using Bing Search API
-type BingProvider struct {
- apiURL string
-}
-
-// NewBingProvider creates a new Bing provider
-func NewBingProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &BingProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *BingProvider) Name() string {
- return "bing"
-}
-
-// Search performs a web search using Bing Search API
-func (p *BingProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Bing Search API
- return nil, fmt.Errorf("bing search provider is not yet implemented")
-}
diff --git a/internal/application/service/web_search/bocha.go b/internal/application/service/web_search/bocha.go
deleted file mode 100644
index 7f55b3752..000000000
--- a/internal/application/service/web_search/bocha.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// BochaProvider implements web search using Bocha API
-type BochaProvider struct {
- apiKey string
- apiURL string
-}
-
-// NewBochaProvider creates a new Bocha provider
-func NewBochaProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &BochaProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *BochaProvider) Name() string {
- return "bocha"
-}
-
-// Search performs a web search using Bocha API
-func (p *BochaProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Bocha search API
- return nil, fmt.Errorf("bocha search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *BochaProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
diff --git a/internal/application/service/web_search/duckduckgo.go b/internal/application/service/web_search/duckduckgo.go
new file mode 100644
index 000000000..6c97b6443
--- /dev/null
+++ b/internal/application/service/web_search/duckduckgo.go
@@ -0,0 +1,230 @@
+package web_search
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/Tencent/WeKnora/internal/config"
+ "github.com/Tencent/WeKnora/internal/logger"
+ "github.com/Tencent/WeKnora/internal/types"
+ "github.com/Tencent/WeKnora/internal/types/interfaces"
+)
+
+// DuckDuckGoProvider implements web search using DuckDuckGo (HTML first, API fallback)
+type DuckDuckGoProvider struct {
+ client *http.Client
+}
+
+// NewDuckDuckGoProvider creates a new DuckDuckGo provider
+func NewDuckDuckGoProvider(_ config.WebSearchProviderConfig) (interfaces.WebSearchProvider, error) {
+ return &DuckDuckGoProvider{
+ client: &http.Client{
+ Timeout: 30 * time.Second,
+ },
+ }, nil
+}
+
+// Name returns the provider name
+func (p *DuckDuckGoProvider) Name() string {
+ return "duckduckgo"
+}
+
+// Search performs a web search using DuckDuckGo HTML endpoint with API fallback
+func (p *DuckDuckGoProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
+ if maxResults <= 0 {
+ maxResults = 5
+ }
+ // Try HTML scraping first (more reliable for general results)
+ htmlResults, err := p.searchHTML(ctx, query, maxResults)
+ if err == nil && len(htmlResults) > 0 {
+ return htmlResults, nil
+ }
+ // Fallback to Instant Answer API
+ apiResults, apiErr := p.searchAPI(ctx, query, maxResults)
+ if apiErr == nil && len(apiResults) > 0 {
+ return apiResults, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("duckduckgo HTML search failed: %w", err)
+ }
+ return nil, fmt.Errorf("duckduckgo API search failed: %w", apiErr)
+}
+
+func (p *DuckDuckGoProvider) searchHTML(ctx context.Context, query string, maxResults int) ([]*types.WebSearchResult, error) {
+ baseURL := "https://html.duckduckgo.com/html/"
+ params := url.Values{}
+ params.Set("q", query)
+ // Prefer Chinese results if applicable; otherwise DDG will auto-detect
+ params.Set("kl", "cn-zh")
+
+ reqURL := baseURL + "?" + params.Encode()
+ req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+ // Use a realistic UA to avoid blocks
+ req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
+
+ resp, err := p.client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to perform request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
+ return nil, fmt.Errorf("duckduckgo HTML returned status %d", resp.StatusCode)
+ }
+
+ doc, err := goquery.NewDocumentFromReader(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse HTML: %w", err)
+ }
+
+ results := make([]*types.WebSearchResult, 0, maxResults)
+ // Structure based on DDG HTML page
+ doc.Find(".web-result").Each(func(i int, s *goquery.Selection) {
+ if len(results) >= maxResults {
+ return
+ }
+ titleNode := s.Find(".result__a")
+ title := strings.TrimSpace(titleNode.Text())
+ var link string
+ if href, exists := titleNode.Attr("href"); exists {
+ link = cleanDDGURL(href)
+ }
+ snippet := strings.TrimSpace(s.Find(".result__snippet").Text())
+ if title != "" && link != "" {
+ results = append(results, &types.WebSearchResult{
+ Title: title,
+ URL: link,
+ Snippet: snippet,
+ Source: "duckduckgo",
+ })
+ }
+ })
+
+ logger.Infof(ctx, "DuckDuckGo HTML search returned %d results for query: %s", len(results), query)
+ return results, nil
+}
+
+func (p *DuckDuckGoProvider) searchAPI(ctx context.Context, query string, maxResults int) ([]*types.WebSearchResult, error) {
+ baseURL := "https://api.duckduckgo.com/"
+ params := url.Values{}
+ params.Set("q", query)
+ params.Set("format", "json")
+ params.Set("no_html", "1")
+ params.Set("skip_disambig", "1")
+
+ reqURL := baseURL + "?" + params.Encode()
+ req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+ req.Header.Set("User-Agent", "WeKnora/1.0")
+
+ resp, err := p.client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to perform request: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("duckduckgo API returned status %d: %s", resp.StatusCode, string(body))
+ }
+
+ var apiResponse struct {
+ AbstractText string `json:"AbstractText"`
+ AbstractURL string `json:"AbstractURL"`
+ Heading string `json:"Heading"`
+ RelatedTopics []struct {
+ FirstURL string `json:"FirstURL"`
+ Text string `json:"Text"`
+ } `json:"RelatedTopics"`
+ Results []struct {
+ FirstURL string `json:"FirstURL"`
+ Text string `json:"Text"`
+ } `json:"Results"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
+ return nil, fmt.Errorf("failed to decode API response: %w", err)
+ }
+
+ results := make([]*types.WebSearchResult, 0, maxResults)
+ if apiResponse.AbstractText != "" && apiResponse.AbstractURL != "" {
+ results = append(results, &types.WebSearchResult{
+ Title: apiResponse.Heading,
+ URL: apiResponse.AbstractURL,
+ Snippet: apiResponse.AbstractText,
+ Source: "duckduckgo",
+ })
+ }
+ for _, topic := range apiResponse.RelatedTopics {
+ if len(results) >= maxResults {
+ break
+ }
+ if topic.Text != "" && topic.FirstURL != "" {
+ results = append(results, &types.WebSearchResult{
+ Title: extractTitle(topic.Text),
+ URL: topic.FirstURL,
+ Snippet: topic.Text,
+ Source: "duckduckgo",
+ })
+ }
+ }
+ for _, r := range apiResponse.Results {
+ if len(results) >= maxResults {
+ break
+ }
+ if r.Text != "" && r.FirstURL != "" {
+ results = append(results, &types.WebSearchResult{
+ Title: extractTitle(r.Text),
+ URL: r.FirstURL,
+ Snippet: r.Text,
+ Source: "duckduckgo",
+ })
+ }
+ }
+
+ logger.Infof(ctx, "DuckDuckGo API search returned %d results for query: %s", len(results), query)
+ return results, nil
+}
+
+func cleanDDGURL(urlStr string) string {
+ if strings.HasPrefix(urlStr, "//duckduckgo.com/l/?uddg=") {
+ trimmed := strings.TrimPrefix(urlStr, "//duckduckgo.com/l/?uddg=")
+ if idx := strings.Index(trimmed, "&rut="); idx != -1 {
+ decodedStr, err := url.PathUnescape(trimmed[:idx])
+ if err == nil {
+ return decodedStr
+ }
+ return ""
+ }
+ }
+ if strings.HasPrefix(urlStr, "https://duckduckgo.com/l/?uddg=") {
+ if parsedURL, err := url.Parse(urlStr); err == nil {
+ if uddg := parsedURL.Query().Get("uddg"); uddg != "" {
+ return uddg
+ }
+ }
+ }
+ return urlStr
+}
+
+func extractTitle(text string) string {
+ lines := strings.Split(text, "\n")
+ if len(lines) > 0 {
+ title := strings.TrimSpace(lines[0])
+ if len(title) > 100 {
+ title = title[:100] + "..."
+ }
+ return title
+ }
+ return strings.TrimSpace(text)
+}
diff --git a/internal/application/service/web_search/duckduckgo_test.go b/internal/application/service/web_search/duckduckgo_test.go
new file mode 100644
index 000000000..9a79342b9
--- /dev/null
+++ b/internal/application/service/web_search/duckduckgo_test.go
@@ -0,0 +1,154 @@
+package web_search
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/Tencent/WeKnora/internal/config"
+)
+
+// testRoundTripper rewrites outgoing requests that target DuckDuckGo hosts
+// to the provided test server, preserving path and query.
+type testRoundTripper struct {
+ base *url.URL
+ next http.RoundTripper
+}
+
+func (t *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ // Only rewrite requests to duckduckgo hosts used by the provider
+ if req.URL.Host == "html.duckduckgo.com" || req.URL.Host == "api.duckduckgo.com" {
+ cloned := *req
+ u := *req.URL
+ u.Scheme = t.base.Scheme
+ u.Host = t.base.Host
+ // Keep original path; our test server handlers should register for the same paths.
+ cloned.URL = &u
+ req = &cloned
+ }
+ return t.next.RoundTrip(req)
+}
+
+func newTestClient(ts *httptest.Server) *http.Client {
+ baseURL, _ := url.Parse(ts.URL)
+ return &http.Client{
+ Timeout: 5 * time.Second,
+ Transport: &testRoundTripper{
+ base: baseURL,
+ next: http.DefaultTransport,
+ },
+ }
+}
+
+func TestDuckDuckGoProvider_Name(t *testing.T) {
+ p, _ := NewDuckDuckGoProvider(config.WebSearchProviderConfig{})
+ if p.Name() != "duckduckgo" {
+ t.Fatalf("expected provider name duckduckgo, got %s", p.Name())
+ }
+}
+
+func TestDuckDuckGoProvider_Search_HTMLSuccess(t *testing.T) {
+ // Minimal HTML page with two results, matching selectors used in searchHTML
+ html := `
+
+
+
+
+
+`
+
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Provider requests GET https://html.duckduckgo.com/html/?q=...&kl=...
+ if r.URL.Path == "/html/" {
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(html))
+ return
+ }
+ t.Fatalf("unexpected request path: %s", r.URL.Path)
+ }))
+ defer ts.Close()
+
+ // Build provider and inject our test client
+ prov, _ := NewDuckDuckGoProvider(config.WebSearchProviderConfig{})
+ dp := prov.(*DuckDuckGoProvider)
+ dp.client = newTestClient(ts)
+
+ ctx := context.Background()
+ results, err := dp.Search(ctx, "weknora", 5, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(results) != 2 {
+ t.Fatalf("expected 2 results, got %d", len(results))
+ }
+ if results[0].Title != "Example One" || !strings.HasPrefix(results[0].URL, "https://example.com/") || results[0].Snippet != "Snippet one" {
+ t.Fatalf("unexpected first result: %+v", results[0])
+ }
+ if results[1].Title != "Example Two" || !strings.HasPrefix(results[1].URL, "https://example.org/") || results[1].Snippet != "Snippet two" {
+ t.Fatalf("unexpected second result: %+v", results[1])
+ }
+}
+
+func TestDuckDuckGoProvider_Search_APIFallback(t *testing.T) {
+ // Simulate HTML returning non-OK to force API fallback, then a minimal API JSON
+ apiResp := struct {
+ AbstractText string `json:"AbstractText"`
+ AbstractURL string `json:"AbstractURL"`
+ Heading string `json:"Heading"`
+ Results []struct {
+ FirstURL string `json:"FirstURL"`
+ Text string `json:"Text"`
+ } `json:"Results"`
+ }{
+ AbstractText: "Abstract snippet",
+ AbstractURL: "https://example.com/abstract",
+ Heading: "Abstract Heading",
+ Results: []struct {
+ FirstURL string `json:"FirstURL"`
+ Text string `json:"Text"`
+ }{
+ {FirstURL: "https://example.net/x", Text: "Title X - Detail X"},
+ },
+ }
+
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/html/":
+ // Force fallback by returning 500
+ w.WriteHeader(http.StatusInternalServerError)
+ default:
+ // API endpoint path "/"
+ w.Header().Set("Content-Type", "application/json")
+ enc := json.NewEncoder(w)
+ _ = enc.Encode(apiResp)
+ }
+ }))
+ defer ts.Close()
+
+ prov, _ := NewDuckDuckGoProvider(config.WebSearchProviderConfig{})
+ dp := prov.(*DuckDuckGoProvider)
+ dp.client = newTestClient(ts)
+
+ ctx := context.Background()
+ results, err := dp.Search(ctx, "weknora", 3, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(results) == 0 {
+ t.Fatalf("expected some results from API fallback")
+ }
+ if results[0].URL != "https://example.com/abstract" || results[0].Title != "Abstract Heading" {
+ t.Fatalf("unexpected first API result: %+v", results[0])
+ }
+}
diff --git a/internal/application/service/web_search/exa.go b/internal/application/service/web_search/exa.go
deleted file mode 100644
index f15df0379..000000000
--- a/internal/application/service/web_search/exa.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// ExaProvider implements web search using Exa API
-type ExaProvider struct {
- apiKey string
- apiURL string
-}
-
-// NewExaProvider creates a new Exa provider
-func NewExaProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &ExaProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *ExaProvider) Name() string {
- return "exa"
-}
-
-// Search performs a web search using Exa API
-func (p *ExaProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Exa search API
- return nil, fmt.Errorf("exa search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *ExaProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
diff --git a/internal/application/service/web_search/google.go b/internal/application/service/web_search/google.go
deleted file mode 100644
index 3689cb92d..000000000
--- a/internal/application/service/web_search/google.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// GoogleProvider implements web search using Google Custom Search API
-type GoogleProvider struct {
- apiKey string
- searchEngineID string
- apiURL string
-}
-
-// NewGoogleProvider creates a new Google provider
-func NewGoogleProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &GoogleProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *GoogleProvider) Name() string {
- return "google"
-}
-
-// Search performs a web search using Google Custom Search API
-func (p *GoogleProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Google Custom Search API
- return nil, fmt.Errorf("google search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key and search engine ID
-func (p *GoogleProvider) SetAPIKey(apiKey, searchEngineID string) {
- p.apiKey = apiKey
- p.searchEngineID = searchEngineID
-}
diff --git a/internal/application/service/web_search/kuaisou.go b/internal/application/service/web_search/kuaisou.go
deleted file mode 100644
index 4a0fed9ad..000000000
--- a/internal/application/service/web_search/kuaisou.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package service
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "time"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/logger"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// KuaisouProvider implements web search using Kuaisou API
-type KuaisouProvider struct {
- apiKey string
- apiURL string
- client *http.Client
-}
-
-// NewKuaisouProvider creates a new Kuaisou provider
-func NewKuaisouProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- if cfg.APIURL == "" {
- return nil, fmt.Errorf("kuaisou API URL is required")
- }
-
- return &KuaisouProvider{
- apiKey: "", // Will be set from tenant config
- apiURL: cfg.APIURL,
- client: &http.Client{
- Timeout: 10 * time.Second,
- },
- }, nil
-}
-
-// Name returns the provider name
-func (p *KuaisouProvider) Name() string {
- return "kuaisou"
-}
-
-// Search performs a web search using Kuaisou API
-func (p *KuaisouProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- if p.apiKey == "" {
- return nil, fmt.Errorf("kuaisou API key is required")
- }
-
- // Prepare request
- reqBody := map[string]interface{}{
- "query": query,
- "max_results": maxResults,
- }
-
- jsonData, err := json.Marshal(reqBody)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal request: %w", err)
- }
-
- req, err := http.NewRequestWithContext(ctx, "POST", p.apiURL, bytes.NewBuffer(jsonData))
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %w", err)
- }
-
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", "Bearer "+p.apiKey)
-
- // Send request
- resp, err := p.client.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to send request: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- return nil, fmt.Errorf("kuaisou API returned status %d: %s", resp.StatusCode, string(body))
- }
-
- // Parse response
- var response struct {
- Results []struct {
- Title string `json:"title"`
- URL string `json:"url"`
- Snippet string `json:"snippet"`
- } `json:"results"`
- }
-
- if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
- return nil, fmt.Errorf("failed to decode response: %w", err)
- }
-
- // Convert to WebSearchResult
- results := make([]*types.WebSearchResult, 0, len(response.Results))
- for _, item := range response.Results {
- results = append(results, &types.WebSearchResult{
- Title: item.Title,
- URL: item.URL,
- Snippet: item.Snippet,
- Source: "kuaisou",
- })
- }
-
- logger.Infof(ctx, "Kuaisou search returned %d results for query: %s", len(results), query)
- return results, nil
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *KuaisouProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
diff --git a/internal/application/service/web_search/searxng.go b/internal/application/service/web_search/searxng.go
deleted file mode 100644
index d2f5d2837..000000000
--- a/internal/application/service/web_search/searxng.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// SearxngProvider implements web search using Searxng API
-type SearxngProvider struct {
- apiURL string
-}
-
-// NewSearxngProvider creates a new Searxng provider
-func NewSearxngProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &SearxngProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *SearxngProvider) Name() string {
- return "searxng"
-}
-
-// Search performs a web search using Searxng API
-func (p *SearxngProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Searxng search API
- return nil, fmt.Errorf("searxng search provider is not yet implemented")
-}
diff --git a/internal/application/service/web_search/tavily.go b/internal/application/service/web_search/tavily.go
deleted file mode 100644
index 700c74a26..000000000
--- a/internal/application/service/web_search/tavily.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// TavilyProvider implements web search using Tavily API
-type TavilyProvider struct {
- apiKey string
- apiURL string
-}
-
-// NewTavilyProvider creates a new Tavily provider
-func NewTavilyProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &TavilyProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *TavilyProvider) Name() string {
- return "tavily"
-}
-
-// Search performs a web search using Tavily API
-func (p *TavilyProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Tavily search API
- return nil, fmt.Errorf("tavily search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *TavilyProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
diff --git a/internal/application/service/web_search/zhipu.go b/internal/application/service/web_search/zhipu.go
deleted file mode 100644
index 91b488a3d..000000000
--- a/internal/application/service/web_search/zhipu.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// ZhipuProvider implements web search using Zhipu API
-type ZhipuProvider struct {
- apiKey string
- apiURL string
-}
-
-// NewZhipuProvider creates a new Zhipu provider
-func NewZhipuProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &ZhipuProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *ZhipuProvider) Name() string {
- return "zhipu"
-}
-
-// Search performs a web search using Zhipu API
-func (p *ZhipuProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Zhipu search API
- return nil, fmt.Errorf("zhipu search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *ZhipuProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
diff --git a/internal/application/service/web_search_baidu.go b/internal/application/service/web_search_baidu.go
deleted file mode 100644
index 433454573..000000000
--- a/internal/application/service/web_search_baidu.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// BaiduProvider implements web search using Baidu API
-type BaiduProvider struct {
- apiURL string
-}
-
-// NewBaiduProvider creates a new Baidu provider
-func NewBaiduProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &BaiduProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *BaiduProvider) Name() string {
- return "baidu"
-}
-
-// Search performs a web search using Baidu API
-func (p *BaiduProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Baidu search API
- return nil, fmt.Errorf("baidu search provider is not yet implemented")
-}
-
diff --git a/internal/application/service/web_search_bing.go b/internal/application/service/web_search_bing.go
deleted file mode 100644
index 510d443e9..000000000
--- a/internal/application/service/web_search_bing.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// BingProvider implements web search using Bing Search API
-type BingProvider struct {
- apiURL string
-}
-
-// NewBingProvider creates a new Bing provider
-func NewBingProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &BingProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *BingProvider) Name() string {
- return "bing"
-}
-
-// Search performs a web search using Bing Search API
-func (p *BingProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Bing Search API
- return nil, fmt.Errorf("bing search provider is not yet implemented")
-}
-
diff --git a/internal/application/service/web_search_bocha.go b/internal/application/service/web_search_bocha.go
deleted file mode 100644
index 6b5b08ace..000000000
--- a/internal/application/service/web_search_bocha.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// BochaProvider implements web search using Bocha API
-type BochaProvider struct {
- apiKey string
- apiURL string
-}
-
-// NewBochaProvider creates a new Bocha provider
-func NewBochaProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &BochaProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *BochaProvider) Name() string {
- return "bocha"
-}
-
-// Search performs a web search using Bocha API
-func (p *BochaProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Bocha search API
- return nil, fmt.Errorf("bocha search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *BochaProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
-
diff --git a/internal/application/service/web_search_exa.go b/internal/application/service/web_search_exa.go
deleted file mode 100644
index cc9c7d5a6..000000000
--- a/internal/application/service/web_search_exa.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// ExaProvider implements web search using Exa API
-type ExaProvider struct {
- apiKey string
- apiURL string
-}
-
-// NewExaProvider creates a new Exa provider
-func NewExaProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &ExaProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *ExaProvider) Name() string {
- return "exa"
-}
-
-// Search performs a web search using Exa API
-func (p *ExaProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Exa search API
- return nil, fmt.Errorf("exa search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *ExaProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
-
diff --git a/internal/application/service/web_search_google.go b/internal/application/service/web_search_google.go
deleted file mode 100644
index 844b48ac5..000000000
--- a/internal/application/service/web_search_google.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// GoogleProvider implements web search using Google Custom Search API
-type GoogleProvider struct {
- apiKey string
- searchEngineID string
- apiURL string
-}
-
-// NewGoogleProvider creates a new Google provider
-func NewGoogleProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &GoogleProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *GoogleProvider) Name() string {
- return "google"
-}
-
-// Search performs a web search using Google Custom Search API
-func (p *GoogleProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Google Custom Search API
- return nil, fmt.Errorf("google search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key and search engine ID
-func (p *GoogleProvider) SetAPIKey(apiKey, searchEngineID string) {
- p.apiKey = apiKey
- p.searchEngineID = searchEngineID
-}
-
diff --git a/internal/application/service/web_search_kuaisou.go b/internal/application/service/web_search_kuaisou.go
deleted file mode 100644
index 91ffeb58c..000000000
--- a/internal/application/service/web_search_kuaisou.go
+++ /dev/null
@@ -1,113 +0,0 @@
-package service
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "time"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/logger"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// KuaisouProvider implements web search using Kuaisou API
-type KuaisouProvider struct {
- apiKey string
- apiURL string
- client *http.Client
-}
-
-// NewKuaisouProvider creates a new Kuaisou provider
-func NewKuaisouProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- if cfg.APIURL == "" {
- return nil, fmt.Errorf("kuaisou API URL is required")
- }
-
- return &KuaisouProvider{
- apiKey: "", // Will be set from tenant config
- apiURL: cfg.APIURL,
- client: &http.Client{
- Timeout: 10 * time.Second,
- },
- }, nil
-}
-
-// Name returns the provider name
-func (p *KuaisouProvider) Name() string {
- return "kuaisou"
-}
-
-// Search performs a web search using Kuaisou API
-func (p *KuaisouProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- if p.apiKey == "" {
- return nil, fmt.Errorf("kuaisou API key is required")
- }
-
- // Prepare request
- reqBody := map[string]interface{}{
- "query": query,
- "max_results": maxResults,
- }
-
- jsonData, err := json.Marshal(reqBody)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal request: %w", err)
- }
-
- req, err := http.NewRequestWithContext(ctx, "POST", p.apiURL, bytes.NewBuffer(jsonData))
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %w", err)
- }
-
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", "Bearer "+p.apiKey)
-
- // Send request
- resp, err := p.client.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to send request: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- return nil, fmt.Errorf("kuaisou API returned status %d: %s", resp.StatusCode, string(body))
- }
-
- // Parse response
- var response struct {
- Results []struct {
- Title string `json:"title"`
- URL string `json:"url"`
- Snippet string `json:"snippet"`
- } `json:"results"`
- }
-
- if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
- return nil, fmt.Errorf("failed to decode response: %w", err)
- }
-
- // Convert to WebSearchResult
- results := make([]*types.WebSearchResult, 0, len(response.Results))
- for _, item := range response.Results {
- results = append(results, &types.WebSearchResult{
- Title: item.Title,
- URL: item.URL,
- Snippet: item.Snippet,
- Source: "kuaisou",
- })
- }
-
- logger.Infof(ctx, "Kuaisou search returned %d results for query: %s", len(results), query)
- return results, nil
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *KuaisouProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
-
diff --git a/internal/application/service/web_search_searxng.go b/internal/application/service/web_search_searxng.go
deleted file mode 100644
index bdd820e1b..000000000
--- a/internal/application/service/web_search_searxng.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// SearxngProvider implements web search using Searxng API
-type SearxngProvider struct {
- apiURL string
-}
-
-// NewSearxngProvider creates a new Searxng provider
-func NewSearxngProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &SearxngProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *SearxngProvider) Name() string {
- return "searxng"
-}
-
-// Search performs a web search using Searxng API
-func (p *SearxngProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Searxng search API
- return nil, fmt.Errorf("searxng search provider is not yet implemented")
-}
-
diff --git a/internal/application/service/web_search_tavily.go b/internal/application/service/web_search_tavily.go
deleted file mode 100644
index 365e5fed7..000000000
--- a/internal/application/service/web_search_tavily.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// TavilyProvider implements web search using Tavily API
-type TavilyProvider struct {
- apiKey string
- apiURL string
-}
-
-// NewTavilyProvider creates a new Tavily provider
-func NewTavilyProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &TavilyProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *TavilyProvider) Name() string {
- return "tavily"
-}
-
-// Search performs a web search using Tavily API
-func (p *TavilyProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Tavily search API
- return nil, fmt.Errorf("tavily search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *TavilyProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
-
diff --git a/internal/application/service/web_search_zhipu.go b/internal/application/service/web_search_zhipu.go
deleted file mode 100644
index 208fe7d68..000000000
--- a/internal/application/service/web_search_zhipu.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
-
- "github.com/Tencent/WeKnora/internal/config"
- "github.com/Tencent/WeKnora/internal/types"
-)
-
-// ZhipuProvider implements web search using Zhipu API
-type ZhipuProvider struct {
- apiKey string
- apiURL string
-}
-
-// NewZhipuProvider creates a new Zhipu provider
-func NewZhipuProvider(cfg config.WebSearchProviderConfig) (WebSearchProvider, error) {
- return &ZhipuProvider{
- apiURL: cfg.APIURL,
- }, nil
-}
-
-// Name returns the provider name
-func (p *ZhipuProvider) Name() string {
- return "zhipu"
-}
-
-// Search performs a web search using Zhipu API
-func (p *ZhipuProvider) Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error) {
- // TODO: Implement Zhipu search API
- return nil, fmt.Errorf("zhipu search provider is not yet implemented")
-}
-
-// SetAPIKey sets the API key for the provider
-func (p *ZhipuProvider) SetAPIKey(apiKey string) {
- p.apiKey = apiKey
-}
-
diff --git a/internal/container/container.go b/internal/container/container.go
index 8a28bad10..efb6101d4 100644
--- a/internal/container/container.go
+++ b/internal/container/container.go
@@ -128,6 +128,11 @@ func BuildContainer(container *dig.Container) *dig.Container {
// Chat pipeline components for processing chat requests
must(container.Provide(chatpipline.NewEventManager))
+ // Ensure Async task components are registered before invoking plugins that depend on KnowledgeService
+ // KnowledgeService depends on *asynq.Client, and plugins (like PluginSearch) are invoked below.
+ must(container.Provide(router.NewAsyncqClient))
+ must(container.Provide(router.NewAsynqServer))
+ must(container.Invoke(router.RunAsynqServer))
must(container.Invoke(chatpipline.NewPluginTracing))
must(container.Invoke(chatpipline.NewPluginSearch))
must(container.Invoke(chatpipline.NewPluginRerank))
@@ -159,9 +164,6 @@ func BuildContainer(container *dig.Container) *dig.Container {
// Router configuration
must(container.Provide(router.NewRouter))
- must(container.Provide(router.NewAsyncqClient))
- must(container.Provide(router.NewAsynqServer))
- must(container.Invoke(router.RunAsynqServer))
return container
}
diff --git a/internal/handler/session/qa.go b/internal/handler/session/qa.go
index 1a7e3e24a..ca25167ff 100644
--- a/internal/handler/session/qa.go
+++ b/internal/handler/session/qa.go
@@ -203,11 +203,16 @@ func (h *Handler) AgentQA(c *gin.Context) {
}
// Check if agent mode has changed
- currentAgentEnabled := session.AgentConfig.Enabled
+ currentAgentEnabled := session.AgentConfig.AgentModeEnabled
if request.AgentEnabled != currentAgentEnabled {
logger.Infof(ctx, "Agent mode changed from %v to %v", currentAgentEnabled, request.AgentEnabled)
configChanged = true
}
+ currentWebSearchEnabled := session.AgentConfig.AgentModeEnabled
+ if request.WebSearchEnabled != currentWebSearchEnabled {
+ logger.Infof(ctx, "Web search mode changed from %v to %v", currentWebSearchEnabled, request.WebSearchEnabled)
+ configChanged = true
+ }
// If configuration changed, clear context and update session
if configChanged {
@@ -219,8 +224,12 @@ func (h *Handler) AgentQA(c *gin.Context) {
// Continue anyway - this is not a fatal error
}
}
+ if knowledgeBasesChanged {
+ // todo clear temp kb
+ }
session.AgentConfig.KnowledgeBases = request.KnowledgeBaseIDs
- session.AgentConfig.Enabled = request.AgentEnabled
+ session.AgentConfig.AgentModeEnabled = request.AgentEnabled
+ session.AgentConfig.WebSearchEnabled = request.WebSearchEnabled
// Persist the session changes
if err := h.sessionService.UpdateSession(ctx, session); err != nil {
logger.Errorf(ctx, "Failed to update session %s: %v", sessionID, err)
diff --git a/internal/router/task.go b/internal/router/task.go
index 547c4f1d9..55dee374f 100644
--- a/internal/router/task.go
+++ b/internal/router/task.go
@@ -29,10 +29,14 @@ func getAsynqRedisClientOpt() *asynq.RedisClientOpt {
return opt
}
-func NewAsyncqClient() *asynq.Client {
+func NewAsyncqClient() (*asynq.Client, error) {
opt := getAsynqRedisClientOpt()
client := asynq.NewClient(opt)
- return client
+ err := client.Ping()
+ if err != nil {
+ return nil, err
+ }
+ return client, nil
}
func NewAsynqServer() *asynq.Server {
diff --git a/internal/types/agent.go b/internal/types/agent.go
index 74e4651e8..993833f61 100644
--- a/internal/types/agent.go
+++ b/internal/types/agent.go
@@ -24,8 +24,9 @@ type AgentConfig struct {
// SessionAgentConfig represents session-level agent configuration
// Sessions only store Enabled and KnowledgeBases; other configs are read from Tenant at runtime
type SessionAgentConfig struct {
- Enabled bool `json:"enabled"` // Whether agent mode is enabled for this session
- KnowledgeBases []string `json:"knowledge_bases"` // Accessible knowledge base IDs for this session
+ AgentModeEnabled bool `json:"agent_mode_enabled"` // Whether agent mode is enabled for this session
+ WebSearchEnabled bool `json:"web_search_enabled"` // Whether web search is enabled for this session
+ KnowledgeBases []string `json:"knowledge_bases"` // Accessible knowledge base IDs for this session
}
// Value implements driver.Valuer interface for AgentConfig
diff --git a/internal/types/interfaces/knowledge.go b/internal/types/interfaces/knowledge.go
index db798e69c..c1eeac5d8 100644
--- a/internal/types/interfaces/knowledge.go
+++ b/internal/types/interfaces/knowledge.go
@@ -22,6 +22,8 @@ type KnowledgeService interface {
CreateKnowledgeFromURL(ctx context.Context, kbID string, url string, enableMultimodel *bool) (*types.Knowledge, error)
// CreateKnowledgeFromPassage creates knowledge from text passages.
CreateKnowledgeFromPassage(ctx context.Context, kbID string, passage []string) (*types.Knowledge, error)
+ // CreateKnowledgeFromPassageSync creates knowledge from text passages and waits until chunks are indexed.
+ CreateKnowledgeFromPassageSync(ctx context.Context, kbID string, passage []string) (*types.Knowledge, error)
// GetKnowledgeByID retrieves knowledge by ID.
GetKnowledgeByID(ctx context.Context, id string) (*types.Knowledge, error)
// GetKnowledgeBatch retrieves a batch of knowledge by IDs.
diff --git a/internal/types/interfaces/web_search.go b/internal/types/interfaces/web_search.go
index 6e5ecd706..a7814a7a0 100644
--- a/internal/types/interfaces/web_search.go
+++ b/internal/types/interfaces/web_search.go
@@ -6,7 +6,17 @@ import (
"github.com/Tencent/WeKnora/internal/types"
)
-// WebSearchService defines the interface for web search service
+// WebSearchProvider defines the interface for web search providers
+type WebSearchProvider interface {
+ Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error)
+ Name() string
+}
+
type WebSearchService interface {
Search(ctx context.Context, config *types.WebSearchConfig, query string) ([]*types.WebSearchResult, error)
+ CompressWithRAG(ctx context.Context, sessionID string, tempKBID string, questions []string,
+ webSearchResults []*types.WebSearchResult, cfg *types.WebSearchConfig,
+ kbSvc KnowledgeBaseService, knowSvc KnowledgeService,
+ seenURLs map[string]bool, knowledgeIDs []string,
+ ) (compressed []*types.WebSearchResult, kbID string, newSeen map[string]bool, newIDs []string, err error)
}
diff --git a/internal/types/knowledgebase.go b/internal/types/knowledgebase.go
index c2a42ec2d..894626bfd 100644
--- a/internal/types/knowledgebase.go
+++ b/internal/types/knowledgebase.go
@@ -18,6 +18,8 @@ type KnowledgeBase struct {
ID string `yaml:"id" json:"id" gorm:"type:varchar(36);primaryKey"`
// Name of the knowledge base
Name string `yaml:"name" json:"name"`
+ // Whether this knowledge base is temporary (ephemeral) and should be hidden from UI
+ IsTemporary bool `yaml:"is_temporary" json:"is_temporary" gorm:"default:false"`
// Description of the knowledge base
Description string `yaml:"description" json:"description"`
// Tenant ID
diff --git a/internal/types/web_search.go b/internal/types/web_search.go
index d002cf74e..47c1986fa 100644
--- a/internal/types/web_search.go
+++ b/internal/types/web_search.go
@@ -44,7 +44,7 @@ type WebSearchResult struct {
URL string `json:"url"` // 结果URL
Snippet string `json:"snippet"` // 摘要片段
Content string `json:"content"` // 完整内容(可选,需要额外抓取)
- Source string `json:"source"` // 来源(如:kuaisou, baidu等)
+ Source string `json:"source"` // 来源(如:duckduckgo等)
PublishedAt *time.Time `json:"published_at,omitempty"` // 发布时间(如果有)
}
diff --git a/migrations/mysql/07_add_is_temporary.sql b/migrations/mysql/07_add_is_temporary.sql
new file mode 100644
index 000000000..9d5ad7ef6
--- /dev/null
+++ b/migrations/mysql/07_add_is_temporary.sql
@@ -0,0 +1,5 @@
+-- Add is_temporary flag to knowledge_bases to support ephemeral KBs
+ALTER TABLE knowledge_bases
+ ADD COLUMN IF NOT EXISTS is_temporary TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Temporary/hidden KB';
+
+
diff --git a/migrations/paradedb/07_add_is_temporary.sql b/migrations/paradedb/07_add_is_temporary.sql
new file mode 100644
index 000000000..0fff62cac
--- /dev/null
+++ b/migrations/paradedb/07_add_is_temporary.sql
@@ -0,0 +1,5 @@
+-- Add is_temporary flag to knowledge_bases to support ephemeral KBs
+ALTER TABLE knowledge_bases
+ ADD COLUMN IF NOT EXISTS is_temporary BOOLEAN NOT NULL DEFAULT FALSE;
+
+