ninegrid2 6.500.0 → 6.502.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/aiContainer.js +151 -2
- package/dist/bundle.cjs.js +151 -2
- package/dist/bundle.esm.js +151 -2
- package/package.json +1 -1
- package/src/ai/aiContainer.js +151 -2
package/dist/ai/aiContainer.js
CHANGED
|
@@ -2,7 +2,10 @@ import ninegrid from "../index.js";
|
|
|
2
2
|
|
|
3
3
|
class aiContainer extends HTMLElement
|
|
4
4
|
{
|
|
5
|
-
|
|
5
|
+
#target;
|
|
6
|
+
#ing = false;
|
|
7
|
+
//#elChat;
|
|
8
|
+
|
|
6
9
|
constructor() {
|
|
7
10
|
super();
|
|
8
11
|
this.attachShadow({ mode: 'open' });
|
|
@@ -47,13 +50,159 @@ class aiContainer extends HTMLElement
|
|
|
47
50
|
});
|
|
48
51
|
};
|
|
49
52
|
|
|
53
|
+
get target() {
|
|
54
|
+
return this.#target;
|
|
55
|
+
};
|
|
56
|
+
set target(v) {
|
|
57
|
+
this.#target = v;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#getColumnInfo = () => {
|
|
61
|
+
let colInfo = "";
|
|
62
|
+
|
|
63
|
+
console.log(this.#target, this.#target.tagName);
|
|
64
|
+
|
|
65
|
+
this.#target.columns.info().forEach(info => {
|
|
66
|
+
colInfo += `- "${info.name}": ${info.desc}, ${info.type}\n`;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(colInfo);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
#generateQdrantFilter = async (userInput) => {
|
|
73
|
+
|
|
74
|
+
//console.log(document.querySelector("nine-grid").body.querySelector(`thead [data-col="6"]`));
|
|
75
|
+
//console.log(document.querySelector("nine-grid").columns.info());
|
|
76
|
+
|
|
77
|
+
let colInfo = "";
|
|
78
|
+
document.querySelector("nine-grid").columns.info().forEach(info => {
|
|
79
|
+
colInfo += `- "${info.name}": ${info.desc}, ${info.type}\n`;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
//console.log(colInfo);
|
|
83
|
+
|
|
84
|
+
// Qdrant 필터로 변환하기 위한 프롬프트 엔지니어링
|
|
85
|
+
// 중요: 실제 컬렉션의 payload 필드와 그 데이터 타입을 정확히 알려줘야 Gemini가 올바른 필터를 생성합니다.
|
|
86
|
+
const prompt = `
|
|
87
|
+
자연어 쿼리를 Qdrant 필터 JSON 객체로 변환하는 AI 비서입니다.
|
|
88
|
+
|
|
89
|
+
Qdrant 컬렉션에서 사용 가능한 메타데이터 필드와 유형은 다음과 같습니다.:
|
|
90
|
+
${colInfo}
|
|
91
|
+
|
|
92
|
+
필터 생성 규칙:
|
|
93
|
+
1. 위에 제공된 필드만 사용하십시오. 새로운 필드를 만들지 마십시오.
|
|
94
|
+
2. 조건이 명확하게 지정되지 않았거나 모호한 경우 필터에 포함하지 마십시오.
|
|
95
|
+
3. AND 조건에는 'must', OR 조건에는 'should', NOT 조건에는 'must_not'을 사용하십시오.
|
|
96
|
+
4. 문자열 일치에는 'match.text'를 사용하십시오.
|
|
97
|
+
5. 숫자 범위에는 'range.gte', 'range.lte', 'range.gt', 'range.lt'를 사용하십시오.
|
|
98
|
+
6. 특정 값을 포함하는 배열에는 'contains', 'match.any' 또는 'match.all'을 사용하십시오.
|
|
99
|
+
7. 출력은 Qdrant 필터를 나타내는 유효한 JSON 객체여야 합니다.
|
|
100
|
+
8. 적용 가능한 필터가 없는 경우 빈 JSON 객체인 {}를 반환합니다.
|
|
101
|
+
|
|
102
|
+
예:
|
|
103
|
+
- "500달러 미만의 전자제품 찾기" -> {"must": [{"key": "category", "match": {"text": "electronics"}}, {"key": "price", "range": {"lt": 500}}]}
|
|
104
|
+
- "John Doe 또는 Jane Smith의 책 보기" -> {"should": [{"key": "author", "match": {"text": "John Doe"}}, {"key": "author", "match": {"text": "Jane Smith"}}]}
|
|
105
|
+
- "재고 있는 품목(의류 제외)" -> {"must": [{"key": "in_stock", "match": {"text": true}}], "must_not": [{"key": "category", "match": {"text": "clothing"}}]}
|
|
106
|
+
- "다음이 포함된 제품 'new_arrival' 태그" -> {"must": [{"key": "tags", "match": {"text": "new_arrival"}}]}
|
|
107
|
+
- "2020년 이후 출판된 도서" -> {"must": [{"key": "published_year", "range": {"gt": 2020}}]}
|
|
108
|
+
|
|
109
|
+
이제 다음 사용자 쿼리를 변환해 보겠습니다.
|
|
110
|
+
사용자 쿼리: "${userInput}"
|
|
111
|
+
|
|
112
|
+
Qdrant 필터 JSON:
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const response = await chatModel.invoke([
|
|
117
|
+
new SystemMessage("You are a helpful assistant."),
|
|
118
|
+
new HumanMessage(prompt),
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
let filterString = response.content.trim();
|
|
122
|
+
if (filterString.startsWith("```json")) {
|
|
123
|
+
filterString = filterString.replace("```json", "");
|
|
124
|
+
const idx = filterString.indexOf("```");
|
|
125
|
+
if (idx > 0) {
|
|
126
|
+
filterString = filterString.substring(0, idx);
|
|
127
|
+
//console.log(filterString.substring(idx+3));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//filterString = filterString.replaceAll('"value"', '"text"')
|
|
131
|
+
console.log("Generated Filter String:", filterString);
|
|
132
|
+
|
|
133
|
+
// Gemini가 JSON 모드나 Structured Output을 지원하지 않는 경우, 직접 파싱 시도
|
|
134
|
+
// 안정적인 JSON 파싱을 위해 'JSON Mode' 또는 Function Calling 사용을 강력히 권장
|
|
135
|
+
// LangChain의 SelfQueryRetriever를 사용하면 이 부분을 자동화할 수 있습니다.
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(filterString);
|
|
138
|
+
} catch (parseError) {
|
|
139
|
+
console.error("Failed to parse filter string as JSON:", parseError);
|
|
140
|
+
console.error("String that failed to parse:", filterString);
|
|
141
|
+
return null; // 파싱 실패 시 null 반환 또는 적절한 에러 처리
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error("Error generating Qdrant filter with Gemini:", error);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#q1 = () => {
|
|
151
|
+
this.#getColumnInfo();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
#q2 = () => {
|
|
155
|
+
this.#getColumnInfo();
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
#q3 = () => {
|
|
159
|
+
this.#getColumnInfo();
|
|
160
|
+
};
|
|
161
|
+
|
|
50
162
|
#init = (info) => {
|
|
163
|
+
|
|
164
|
+
//this.#elChat = this.shadowRoot.querySelector("nx-ai-chat");
|
|
165
|
+
|
|
166
|
+
this.shadowRoot.querySelector("textarea").addEventListener("keydown", this.#keydownHandler);
|
|
167
|
+
|
|
51
168
|
this.shadowRoot.querySelector(".expand-icon").addEventListener("click", this.#toggleCollapseHandler);
|
|
52
169
|
this.shadowRoot.querySelector(".collapse-icon").addEventListener("click", this.#toggleCollapseHandler);
|
|
53
170
|
|
|
54
|
-
this.shadowRoot.querySelectorAll(".menu-icon").forEach(el => el.addEventListener("click", this
|
|
171
|
+
this.shadowRoot.querySelectorAll(".menu-icon").forEach(el => el.addEventListener("click", this.#menuClickHandler));
|
|
55
172
|
};
|
|
56
173
|
|
|
174
|
+
#keydownHandler = (e) => {
|
|
175
|
+
if (e.key !== "Enter") return;
|
|
176
|
+
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
|
|
179
|
+
const question = e.target.value.trim();
|
|
180
|
+
if (!question) return;
|
|
181
|
+
|
|
182
|
+
if (this.#ing) return;
|
|
183
|
+
this.#ing = true;
|
|
184
|
+
|
|
185
|
+
/** setTimeout 없으면, 맥에서 한글 잔상이 남음 */
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
e.target.value = "";
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this.shadowRoot.querySelector("nx-ai-chat").add("me", question);
|
|
191
|
+
this.shadowRoot.querySelector("nx-ai-chat").add("ing", question);
|
|
192
|
+
|
|
193
|
+
if (this.shadowRoot.querySelector(".menu-filter").classList.contains("active")) {
|
|
194
|
+
this.#q1();
|
|
195
|
+
}
|
|
196
|
+
else if (this.shadowRoot.querySelector(".menu-general").classList.contains("active")) {
|
|
197
|
+
this.#q2();
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.#q3();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.#ing = false;
|
|
204
|
+
}
|
|
205
|
+
|
|
57
206
|
#toggleCollapseHandler = () => {
|
|
58
207
|
this.classList.toggle("collapse");
|
|
59
208
|
};
|
package/dist/bundle.cjs.js
CHANGED
|
@@ -27856,7 +27856,10 @@ customElements.define("nx-ai-chat", aiChat);
|
|
|
27856
27856
|
|
|
27857
27857
|
class aiContainer extends HTMLElement
|
|
27858
27858
|
{
|
|
27859
|
-
|
|
27859
|
+
#target;
|
|
27860
|
+
#ing = false;
|
|
27861
|
+
//#elChat;
|
|
27862
|
+
|
|
27860
27863
|
constructor() {
|
|
27861
27864
|
super();
|
|
27862
27865
|
this.attachShadow({ mode: 'open' });
|
|
@@ -27901,13 +27904,159 @@ class aiContainer extends HTMLElement
|
|
|
27901
27904
|
});
|
|
27902
27905
|
};
|
|
27903
27906
|
|
|
27907
|
+
get target() {
|
|
27908
|
+
return this.#target;
|
|
27909
|
+
};
|
|
27910
|
+
set target(v) {
|
|
27911
|
+
this.#target = v;
|
|
27912
|
+
}
|
|
27913
|
+
|
|
27914
|
+
#getColumnInfo = () => {
|
|
27915
|
+
let colInfo = "";
|
|
27916
|
+
|
|
27917
|
+
console.log(this.#target, this.#target.tagName);
|
|
27918
|
+
|
|
27919
|
+
this.#target.columns.info().forEach(info => {
|
|
27920
|
+
colInfo += `- "${info.name}": ${info.desc}, ${info.type}\n`;
|
|
27921
|
+
});
|
|
27922
|
+
|
|
27923
|
+
console.log(colInfo);
|
|
27924
|
+
};
|
|
27925
|
+
|
|
27926
|
+
#generateQdrantFilter = async (userInput) => {
|
|
27927
|
+
|
|
27928
|
+
//console.log(document.querySelector("nine-grid").body.querySelector(`thead [data-col="6"]`));
|
|
27929
|
+
//console.log(document.querySelector("nine-grid").columns.info());
|
|
27930
|
+
|
|
27931
|
+
let colInfo = "";
|
|
27932
|
+
document.querySelector("nine-grid").columns.info().forEach(info => {
|
|
27933
|
+
colInfo += `- "${info.name}": ${info.desc}, ${info.type}\n`;
|
|
27934
|
+
});
|
|
27935
|
+
|
|
27936
|
+
//console.log(colInfo);
|
|
27937
|
+
|
|
27938
|
+
// Qdrant 필터로 변환하기 위한 프롬프트 엔지니어링
|
|
27939
|
+
// 중요: 실제 컬렉션의 payload 필드와 그 데이터 타입을 정확히 알려줘야 Gemini가 올바른 필터를 생성합니다.
|
|
27940
|
+
const prompt = `
|
|
27941
|
+
자연어 쿼리를 Qdrant 필터 JSON 객체로 변환하는 AI 비서입니다.
|
|
27942
|
+
|
|
27943
|
+
Qdrant 컬렉션에서 사용 가능한 메타데이터 필드와 유형은 다음과 같습니다.:
|
|
27944
|
+
${colInfo}
|
|
27945
|
+
|
|
27946
|
+
필터 생성 규칙:
|
|
27947
|
+
1. 위에 제공된 필드만 사용하십시오. 새로운 필드를 만들지 마십시오.
|
|
27948
|
+
2. 조건이 명확하게 지정되지 않았거나 모호한 경우 필터에 포함하지 마십시오.
|
|
27949
|
+
3. AND 조건에는 'must', OR 조건에는 'should', NOT 조건에는 'must_not'을 사용하십시오.
|
|
27950
|
+
4. 문자열 일치에는 'match.text'를 사용하십시오.
|
|
27951
|
+
5. 숫자 범위에는 'range.gte', 'range.lte', 'range.gt', 'range.lt'를 사용하십시오.
|
|
27952
|
+
6. 특정 값을 포함하는 배열에는 'contains', 'match.any' 또는 'match.all'을 사용하십시오.
|
|
27953
|
+
7. 출력은 Qdrant 필터를 나타내는 유효한 JSON 객체여야 합니다.
|
|
27954
|
+
8. 적용 가능한 필터가 없는 경우 빈 JSON 객체인 {}를 반환합니다.
|
|
27955
|
+
|
|
27956
|
+
예:
|
|
27957
|
+
- "500달러 미만의 전자제품 찾기" -> {"must": [{"key": "category", "match": {"text": "electronics"}}, {"key": "price", "range": {"lt": 500}}]}
|
|
27958
|
+
- "John Doe 또는 Jane Smith의 책 보기" -> {"should": [{"key": "author", "match": {"text": "John Doe"}}, {"key": "author", "match": {"text": "Jane Smith"}}]}
|
|
27959
|
+
- "재고 있는 품목(의류 제외)" -> {"must": [{"key": "in_stock", "match": {"text": true}}], "must_not": [{"key": "category", "match": {"text": "clothing"}}]}
|
|
27960
|
+
- "다음이 포함된 제품 'new_arrival' 태그" -> {"must": [{"key": "tags", "match": {"text": "new_arrival"}}]}
|
|
27961
|
+
- "2020년 이후 출판된 도서" -> {"must": [{"key": "published_year", "range": {"gt": 2020}}]}
|
|
27962
|
+
|
|
27963
|
+
이제 다음 사용자 쿼리를 변환해 보겠습니다.
|
|
27964
|
+
사용자 쿼리: "${userInput}"
|
|
27965
|
+
|
|
27966
|
+
Qdrant 필터 JSON:
|
|
27967
|
+
`;
|
|
27968
|
+
|
|
27969
|
+
try {
|
|
27970
|
+
const response = await chatModel.invoke([
|
|
27971
|
+
new SystemMessage("You are a helpful assistant."),
|
|
27972
|
+
new HumanMessage(prompt),
|
|
27973
|
+
]);
|
|
27974
|
+
|
|
27975
|
+
let filterString = response.content.trim();
|
|
27976
|
+
if (filterString.startsWith("```json")) {
|
|
27977
|
+
filterString = filterString.replace("```json", "");
|
|
27978
|
+
const idx = filterString.indexOf("```");
|
|
27979
|
+
if (idx > 0) {
|
|
27980
|
+
filterString = filterString.substring(0, idx);
|
|
27981
|
+
//console.log(filterString.substring(idx+3));
|
|
27982
|
+
}
|
|
27983
|
+
}
|
|
27984
|
+
//filterString = filterString.replaceAll('"value"', '"text"')
|
|
27985
|
+
console.log("Generated Filter String:", filterString);
|
|
27986
|
+
|
|
27987
|
+
// Gemini가 JSON 모드나 Structured Output을 지원하지 않는 경우, 직접 파싱 시도
|
|
27988
|
+
// 안정적인 JSON 파싱을 위해 'JSON Mode' 또는 Function Calling 사용을 강력히 권장
|
|
27989
|
+
// LangChain의 SelfQueryRetriever를 사용하면 이 부분을 자동화할 수 있습니다.
|
|
27990
|
+
try {
|
|
27991
|
+
return JSON.parse(filterString);
|
|
27992
|
+
} catch (parseError) {
|
|
27993
|
+
console.error("Failed to parse filter string as JSON:", parseError);
|
|
27994
|
+
console.error("String that failed to parse:", filterString);
|
|
27995
|
+
return null; // 파싱 실패 시 null 반환 또는 적절한 에러 처리
|
|
27996
|
+
}
|
|
27997
|
+
|
|
27998
|
+
} catch (error) {
|
|
27999
|
+
console.error("Error generating Qdrant filter with Gemini:", error);
|
|
28000
|
+
return null;
|
|
28001
|
+
}
|
|
28002
|
+
}
|
|
28003
|
+
|
|
28004
|
+
#q1 = () => {
|
|
28005
|
+
this.#getColumnInfo();
|
|
28006
|
+
};
|
|
28007
|
+
|
|
28008
|
+
#q2 = () => {
|
|
28009
|
+
this.#getColumnInfo();
|
|
28010
|
+
};
|
|
28011
|
+
|
|
28012
|
+
#q3 = () => {
|
|
28013
|
+
this.#getColumnInfo();
|
|
28014
|
+
};
|
|
28015
|
+
|
|
27904
28016
|
#init = (info) => {
|
|
28017
|
+
|
|
28018
|
+
//this.#elChat = this.shadowRoot.querySelector("nx-ai-chat");
|
|
28019
|
+
|
|
28020
|
+
this.shadowRoot.querySelector("textarea").addEventListener("keydown", this.#keydownHandler);
|
|
28021
|
+
|
|
27905
28022
|
this.shadowRoot.querySelector(".expand-icon").addEventListener("click", this.#toggleCollapseHandler);
|
|
27906
28023
|
this.shadowRoot.querySelector(".collapse-icon").addEventListener("click", this.#toggleCollapseHandler);
|
|
27907
28024
|
|
|
27908
|
-
this.shadowRoot.querySelectorAll(".menu-icon").forEach(el => el.addEventListener("click", this
|
|
28025
|
+
this.shadowRoot.querySelectorAll(".menu-icon").forEach(el => el.addEventListener("click", this.#menuClickHandler));
|
|
27909
28026
|
};
|
|
27910
28027
|
|
|
28028
|
+
#keydownHandler = (e) => {
|
|
28029
|
+
if (e.key !== "Enter") return;
|
|
28030
|
+
|
|
28031
|
+
e.preventDefault();
|
|
28032
|
+
|
|
28033
|
+
const question = e.target.value.trim();
|
|
28034
|
+
if (!question) return;
|
|
28035
|
+
|
|
28036
|
+
if (this.#ing) return;
|
|
28037
|
+
this.#ing = true;
|
|
28038
|
+
|
|
28039
|
+
/** setTimeout 없으면, 맥에서 한글 잔상이 남음 */
|
|
28040
|
+
setTimeout(() => {
|
|
28041
|
+
e.target.value = "";
|
|
28042
|
+
});
|
|
28043
|
+
|
|
28044
|
+
this.shadowRoot.querySelector("nx-ai-chat").add("me", question);
|
|
28045
|
+
this.shadowRoot.querySelector("nx-ai-chat").add("ing", question);
|
|
28046
|
+
|
|
28047
|
+
if (this.shadowRoot.querySelector(".menu-filter").classList.contains("active")) {
|
|
28048
|
+
this.#q1();
|
|
28049
|
+
}
|
|
28050
|
+
else if (this.shadowRoot.querySelector(".menu-general").classList.contains("active")) {
|
|
28051
|
+
this.#q2();
|
|
28052
|
+
}
|
|
28053
|
+
else {
|
|
28054
|
+
this.#q3();
|
|
28055
|
+
}
|
|
28056
|
+
|
|
28057
|
+
this.#ing = false;
|
|
28058
|
+
}
|
|
28059
|
+
|
|
27911
28060
|
#toggleCollapseHandler = () => {
|
|
27912
28061
|
this.classList.toggle("collapse");
|
|
27913
28062
|
};
|
package/dist/bundle.esm.js
CHANGED
|
@@ -27854,7 +27854,10 @@ customElements.define("nx-ai-chat", aiChat);
|
|
|
27854
27854
|
|
|
27855
27855
|
class aiContainer extends HTMLElement
|
|
27856
27856
|
{
|
|
27857
|
-
|
|
27857
|
+
#target;
|
|
27858
|
+
#ing = false;
|
|
27859
|
+
//#elChat;
|
|
27860
|
+
|
|
27858
27861
|
constructor() {
|
|
27859
27862
|
super();
|
|
27860
27863
|
this.attachShadow({ mode: 'open' });
|
|
@@ -27899,13 +27902,159 @@ class aiContainer extends HTMLElement
|
|
|
27899
27902
|
});
|
|
27900
27903
|
};
|
|
27901
27904
|
|
|
27905
|
+
get target() {
|
|
27906
|
+
return this.#target;
|
|
27907
|
+
};
|
|
27908
|
+
set target(v) {
|
|
27909
|
+
this.#target = v;
|
|
27910
|
+
}
|
|
27911
|
+
|
|
27912
|
+
#getColumnInfo = () => {
|
|
27913
|
+
let colInfo = "";
|
|
27914
|
+
|
|
27915
|
+
console.log(this.#target, this.#target.tagName);
|
|
27916
|
+
|
|
27917
|
+
this.#target.columns.info().forEach(info => {
|
|
27918
|
+
colInfo += `- "${info.name}": ${info.desc}, ${info.type}\n`;
|
|
27919
|
+
});
|
|
27920
|
+
|
|
27921
|
+
console.log(colInfo);
|
|
27922
|
+
};
|
|
27923
|
+
|
|
27924
|
+
#generateQdrantFilter = async (userInput) => {
|
|
27925
|
+
|
|
27926
|
+
//console.log(document.querySelector("nine-grid").body.querySelector(`thead [data-col="6"]`));
|
|
27927
|
+
//console.log(document.querySelector("nine-grid").columns.info());
|
|
27928
|
+
|
|
27929
|
+
let colInfo = "";
|
|
27930
|
+
document.querySelector("nine-grid").columns.info().forEach(info => {
|
|
27931
|
+
colInfo += `- "${info.name}": ${info.desc}, ${info.type}\n`;
|
|
27932
|
+
});
|
|
27933
|
+
|
|
27934
|
+
//console.log(colInfo);
|
|
27935
|
+
|
|
27936
|
+
// Qdrant 필터로 변환하기 위한 프롬프트 엔지니어링
|
|
27937
|
+
// 중요: 실제 컬렉션의 payload 필드와 그 데이터 타입을 정확히 알려줘야 Gemini가 올바른 필터를 생성합니다.
|
|
27938
|
+
const prompt = `
|
|
27939
|
+
자연어 쿼리를 Qdrant 필터 JSON 객체로 변환하는 AI 비서입니다.
|
|
27940
|
+
|
|
27941
|
+
Qdrant 컬렉션에서 사용 가능한 메타데이터 필드와 유형은 다음과 같습니다.:
|
|
27942
|
+
${colInfo}
|
|
27943
|
+
|
|
27944
|
+
필터 생성 규칙:
|
|
27945
|
+
1. 위에 제공된 필드만 사용하십시오. 새로운 필드를 만들지 마십시오.
|
|
27946
|
+
2. 조건이 명확하게 지정되지 않았거나 모호한 경우 필터에 포함하지 마십시오.
|
|
27947
|
+
3. AND 조건에는 'must', OR 조건에는 'should', NOT 조건에는 'must_not'을 사용하십시오.
|
|
27948
|
+
4. 문자열 일치에는 'match.text'를 사용하십시오.
|
|
27949
|
+
5. 숫자 범위에는 'range.gte', 'range.lte', 'range.gt', 'range.lt'를 사용하십시오.
|
|
27950
|
+
6. 특정 값을 포함하는 배열에는 'contains', 'match.any' 또는 'match.all'을 사용하십시오.
|
|
27951
|
+
7. 출력은 Qdrant 필터를 나타내는 유효한 JSON 객체여야 합니다.
|
|
27952
|
+
8. 적용 가능한 필터가 없는 경우 빈 JSON 객체인 {}를 반환합니다.
|
|
27953
|
+
|
|
27954
|
+
예:
|
|
27955
|
+
- "500달러 미만의 전자제품 찾기" -> {"must": [{"key": "category", "match": {"text": "electronics"}}, {"key": "price", "range": {"lt": 500}}]}
|
|
27956
|
+
- "John Doe 또는 Jane Smith의 책 보기" -> {"should": [{"key": "author", "match": {"text": "John Doe"}}, {"key": "author", "match": {"text": "Jane Smith"}}]}
|
|
27957
|
+
- "재고 있는 품목(의류 제외)" -> {"must": [{"key": "in_stock", "match": {"text": true}}], "must_not": [{"key": "category", "match": {"text": "clothing"}}]}
|
|
27958
|
+
- "다음이 포함된 제품 'new_arrival' 태그" -> {"must": [{"key": "tags", "match": {"text": "new_arrival"}}]}
|
|
27959
|
+
- "2020년 이후 출판된 도서" -> {"must": [{"key": "published_year", "range": {"gt": 2020}}]}
|
|
27960
|
+
|
|
27961
|
+
이제 다음 사용자 쿼리를 변환해 보겠습니다.
|
|
27962
|
+
사용자 쿼리: "${userInput}"
|
|
27963
|
+
|
|
27964
|
+
Qdrant 필터 JSON:
|
|
27965
|
+
`;
|
|
27966
|
+
|
|
27967
|
+
try {
|
|
27968
|
+
const response = await chatModel.invoke([
|
|
27969
|
+
new SystemMessage("You are a helpful assistant."),
|
|
27970
|
+
new HumanMessage(prompt),
|
|
27971
|
+
]);
|
|
27972
|
+
|
|
27973
|
+
let filterString = response.content.trim();
|
|
27974
|
+
if (filterString.startsWith("```json")) {
|
|
27975
|
+
filterString = filterString.replace("```json", "");
|
|
27976
|
+
const idx = filterString.indexOf("```");
|
|
27977
|
+
if (idx > 0) {
|
|
27978
|
+
filterString = filterString.substring(0, idx);
|
|
27979
|
+
//console.log(filterString.substring(idx+3));
|
|
27980
|
+
}
|
|
27981
|
+
}
|
|
27982
|
+
//filterString = filterString.replaceAll('"value"', '"text"')
|
|
27983
|
+
console.log("Generated Filter String:", filterString);
|
|
27984
|
+
|
|
27985
|
+
// Gemini가 JSON 모드나 Structured Output을 지원하지 않는 경우, 직접 파싱 시도
|
|
27986
|
+
// 안정적인 JSON 파싱을 위해 'JSON Mode' 또는 Function Calling 사용을 강력히 권장
|
|
27987
|
+
// LangChain의 SelfQueryRetriever를 사용하면 이 부분을 자동화할 수 있습니다.
|
|
27988
|
+
try {
|
|
27989
|
+
return JSON.parse(filterString);
|
|
27990
|
+
} catch (parseError) {
|
|
27991
|
+
console.error("Failed to parse filter string as JSON:", parseError);
|
|
27992
|
+
console.error("String that failed to parse:", filterString);
|
|
27993
|
+
return null; // 파싱 실패 시 null 반환 또는 적절한 에러 처리
|
|
27994
|
+
}
|
|
27995
|
+
|
|
27996
|
+
} catch (error) {
|
|
27997
|
+
console.error("Error generating Qdrant filter with Gemini:", error);
|
|
27998
|
+
return null;
|
|
27999
|
+
}
|
|
28000
|
+
}
|
|
28001
|
+
|
|
28002
|
+
#q1 = () => {
|
|
28003
|
+
this.#getColumnInfo();
|
|
28004
|
+
};
|
|
28005
|
+
|
|
28006
|
+
#q2 = () => {
|
|
28007
|
+
this.#getColumnInfo();
|
|
28008
|
+
};
|
|
28009
|
+
|
|
28010
|
+
#q3 = () => {
|
|
28011
|
+
this.#getColumnInfo();
|
|
28012
|
+
};
|
|
28013
|
+
|
|
27902
28014
|
#init = (info) => {
|
|
28015
|
+
|
|
28016
|
+
//this.#elChat = this.shadowRoot.querySelector("nx-ai-chat");
|
|
28017
|
+
|
|
28018
|
+
this.shadowRoot.querySelector("textarea").addEventListener("keydown", this.#keydownHandler);
|
|
28019
|
+
|
|
27903
28020
|
this.shadowRoot.querySelector(".expand-icon").addEventListener("click", this.#toggleCollapseHandler);
|
|
27904
28021
|
this.shadowRoot.querySelector(".collapse-icon").addEventListener("click", this.#toggleCollapseHandler);
|
|
27905
28022
|
|
|
27906
|
-
this.shadowRoot.querySelectorAll(".menu-icon").forEach(el => el.addEventListener("click", this
|
|
28023
|
+
this.shadowRoot.querySelectorAll(".menu-icon").forEach(el => el.addEventListener("click", this.#menuClickHandler));
|
|
27907
28024
|
};
|
|
27908
28025
|
|
|
28026
|
+
#keydownHandler = (e) => {
|
|
28027
|
+
if (e.key !== "Enter") return;
|
|
28028
|
+
|
|
28029
|
+
e.preventDefault();
|
|
28030
|
+
|
|
28031
|
+
const question = e.target.value.trim();
|
|
28032
|
+
if (!question) return;
|
|
28033
|
+
|
|
28034
|
+
if (this.#ing) return;
|
|
28035
|
+
this.#ing = true;
|
|
28036
|
+
|
|
28037
|
+
/** setTimeout 없으면, 맥에서 한글 잔상이 남음 */
|
|
28038
|
+
setTimeout(() => {
|
|
28039
|
+
e.target.value = "";
|
|
28040
|
+
});
|
|
28041
|
+
|
|
28042
|
+
this.shadowRoot.querySelector("nx-ai-chat").add("me", question);
|
|
28043
|
+
this.shadowRoot.querySelector("nx-ai-chat").add("ing", question);
|
|
28044
|
+
|
|
28045
|
+
if (this.shadowRoot.querySelector(".menu-filter").classList.contains("active")) {
|
|
28046
|
+
this.#q1();
|
|
28047
|
+
}
|
|
28048
|
+
else if (this.shadowRoot.querySelector(".menu-general").classList.contains("active")) {
|
|
28049
|
+
this.#q2();
|
|
28050
|
+
}
|
|
28051
|
+
else {
|
|
28052
|
+
this.#q3();
|
|
28053
|
+
}
|
|
28054
|
+
|
|
28055
|
+
this.#ing = false;
|
|
28056
|
+
}
|
|
28057
|
+
|
|
27909
28058
|
#toggleCollapseHandler = () => {
|
|
27910
28059
|
this.classList.toggle("collapse");
|
|
27911
28060
|
};
|
package/package.json
CHANGED
package/src/ai/aiContainer.js
CHANGED
|
@@ -2,7 +2,10 @@ import ninegrid from "../index.js";
|
|
|
2
2
|
|
|
3
3
|
class aiContainer extends HTMLElement
|
|
4
4
|
{
|
|
5
|
-
|
|
5
|
+
#target;
|
|
6
|
+
#ing = false;
|
|
7
|
+
//#elChat;
|
|
8
|
+
|
|
6
9
|
constructor() {
|
|
7
10
|
super();
|
|
8
11
|
this.attachShadow({ mode: 'open' });
|
|
@@ -47,13 +50,159 @@ class aiContainer extends HTMLElement
|
|
|
47
50
|
});
|
|
48
51
|
};
|
|
49
52
|
|
|
53
|
+
get target() {
|
|
54
|
+
return this.#target;
|
|
55
|
+
};
|
|
56
|
+
set target(v) {
|
|
57
|
+
this.#target = v;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#getColumnInfo = () => {
|
|
61
|
+
let colInfo = "";
|
|
62
|
+
|
|
63
|
+
console.log(this.#target, this.#target.tagName);
|
|
64
|
+
|
|
65
|
+
this.#target.columns.info().forEach(info => {
|
|
66
|
+
colInfo += `- "${info.name}": ${info.desc}, ${info.type}\n`;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(colInfo);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
#generateQdrantFilter = async (userInput) => {
|
|
73
|
+
|
|
74
|
+
//console.log(document.querySelector("nine-grid").body.querySelector(`thead [data-col="6"]`));
|
|
75
|
+
//console.log(document.querySelector("nine-grid").columns.info());
|
|
76
|
+
|
|
77
|
+
let colInfo = "";
|
|
78
|
+
document.querySelector("nine-grid").columns.info().forEach(info => {
|
|
79
|
+
colInfo += `- "${info.name}": ${info.desc}, ${info.type}\n`;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
//console.log(colInfo);
|
|
83
|
+
|
|
84
|
+
// Qdrant 필터로 변환하기 위한 프롬프트 엔지니어링
|
|
85
|
+
// 중요: 실제 컬렉션의 payload 필드와 그 데이터 타입을 정확히 알려줘야 Gemini가 올바른 필터를 생성합니다.
|
|
86
|
+
const prompt = `
|
|
87
|
+
자연어 쿼리를 Qdrant 필터 JSON 객체로 변환하는 AI 비서입니다.
|
|
88
|
+
|
|
89
|
+
Qdrant 컬렉션에서 사용 가능한 메타데이터 필드와 유형은 다음과 같습니다.:
|
|
90
|
+
${colInfo}
|
|
91
|
+
|
|
92
|
+
필터 생성 규칙:
|
|
93
|
+
1. 위에 제공된 필드만 사용하십시오. 새로운 필드를 만들지 마십시오.
|
|
94
|
+
2. 조건이 명확하게 지정되지 않았거나 모호한 경우 필터에 포함하지 마십시오.
|
|
95
|
+
3. AND 조건에는 'must', OR 조건에는 'should', NOT 조건에는 'must_not'을 사용하십시오.
|
|
96
|
+
4. 문자열 일치에는 'match.text'를 사용하십시오.
|
|
97
|
+
5. 숫자 범위에는 'range.gte', 'range.lte', 'range.gt', 'range.lt'를 사용하십시오.
|
|
98
|
+
6. 특정 값을 포함하는 배열에는 'contains', 'match.any' 또는 'match.all'을 사용하십시오.
|
|
99
|
+
7. 출력은 Qdrant 필터를 나타내는 유효한 JSON 객체여야 합니다.
|
|
100
|
+
8. 적용 가능한 필터가 없는 경우 빈 JSON 객체인 {}를 반환합니다.
|
|
101
|
+
|
|
102
|
+
예:
|
|
103
|
+
- "500달러 미만의 전자제품 찾기" -> {"must": [{"key": "category", "match": {"text": "electronics"}}, {"key": "price", "range": {"lt": 500}}]}
|
|
104
|
+
- "John Doe 또는 Jane Smith의 책 보기" -> {"should": [{"key": "author", "match": {"text": "John Doe"}}, {"key": "author", "match": {"text": "Jane Smith"}}]}
|
|
105
|
+
- "재고 있는 품목(의류 제외)" -> {"must": [{"key": "in_stock", "match": {"text": true}}], "must_not": [{"key": "category", "match": {"text": "clothing"}}]}
|
|
106
|
+
- "다음이 포함된 제품 'new_arrival' 태그" -> {"must": [{"key": "tags", "match": {"text": "new_arrival"}}]}
|
|
107
|
+
- "2020년 이후 출판된 도서" -> {"must": [{"key": "published_year", "range": {"gt": 2020}}]}
|
|
108
|
+
|
|
109
|
+
이제 다음 사용자 쿼리를 변환해 보겠습니다.
|
|
110
|
+
사용자 쿼리: "${userInput}"
|
|
111
|
+
|
|
112
|
+
Qdrant 필터 JSON:
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const response = await chatModel.invoke([
|
|
117
|
+
new SystemMessage("You are a helpful assistant."),
|
|
118
|
+
new HumanMessage(prompt),
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
let filterString = response.content.trim();
|
|
122
|
+
if (filterString.startsWith("```json")) {
|
|
123
|
+
filterString = filterString.replace("```json", "");
|
|
124
|
+
const idx = filterString.indexOf("```");
|
|
125
|
+
if (idx > 0) {
|
|
126
|
+
filterString = filterString.substring(0, idx);
|
|
127
|
+
//console.log(filterString.substring(idx+3));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//filterString = filterString.replaceAll('"value"', '"text"')
|
|
131
|
+
console.log("Generated Filter String:", filterString);
|
|
132
|
+
|
|
133
|
+
// Gemini가 JSON 모드나 Structured Output을 지원하지 않는 경우, 직접 파싱 시도
|
|
134
|
+
// 안정적인 JSON 파싱을 위해 'JSON Mode' 또는 Function Calling 사용을 강력히 권장
|
|
135
|
+
// LangChain의 SelfQueryRetriever를 사용하면 이 부분을 자동화할 수 있습니다.
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(filterString);
|
|
138
|
+
} catch (parseError) {
|
|
139
|
+
console.error("Failed to parse filter string as JSON:", parseError);
|
|
140
|
+
console.error("String that failed to parse:", filterString);
|
|
141
|
+
return null; // 파싱 실패 시 null 반환 또는 적절한 에러 처리
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error("Error generating Qdrant filter with Gemini:", error);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#q1 = () => {
|
|
151
|
+
this.#getColumnInfo();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
#q2 = () => {
|
|
155
|
+
this.#getColumnInfo();
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
#q3 = () => {
|
|
159
|
+
this.#getColumnInfo();
|
|
160
|
+
};
|
|
161
|
+
|
|
50
162
|
#init = (info) => {
|
|
163
|
+
|
|
164
|
+
//this.#elChat = this.shadowRoot.querySelector("nx-ai-chat");
|
|
165
|
+
|
|
166
|
+
this.shadowRoot.querySelector("textarea").addEventListener("keydown", this.#keydownHandler);
|
|
167
|
+
|
|
51
168
|
this.shadowRoot.querySelector(".expand-icon").addEventListener("click", this.#toggleCollapseHandler);
|
|
52
169
|
this.shadowRoot.querySelector(".collapse-icon").addEventListener("click", this.#toggleCollapseHandler);
|
|
53
170
|
|
|
54
|
-
this.shadowRoot.querySelectorAll(".menu-icon").forEach(el => el.addEventListener("click", this
|
|
171
|
+
this.shadowRoot.querySelectorAll(".menu-icon").forEach(el => el.addEventListener("click", this.#menuClickHandler));
|
|
55
172
|
};
|
|
56
173
|
|
|
174
|
+
#keydownHandler = (e) => {
|
|
175
|
+
if (e.key !== "Enter") return;
|
|
176
|
+
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
|
|
179
|
+
const question = e.target.value.trim();
|
|
180
|
+
if (!question) return;
|
|
181
|
+
|
|
182
|
+
if (this.#ing) return;
|
|
183
|
+
this.#ing = true;
|
|
184
|
+
|
|
185
|
+
/** setTimeout 없으면, 맥에서 한글 잔상이 남음 */
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
e.target.value = "";
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this.shadowRoot.querySelector("nx-ai-chat").add("me", question);
|
|
191
|
+
this.shadowRoot.querySelector("nx-ai-chat").add("ing", question);
|
|
192
|
+
|
|
193
|
+
if (this.shadowRoot.querySelector(".menu-filter").classList.contains("active")) {
|
|
194
|
+
this.#q1();
|
|
195
|
+
}
|
|
196
|
+
else if (this.shadowRoot.querySelector(".menu-general").classList.contains("active")) {
|
|
197
|
+
this.#q2();
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.#q3();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.#ing = false;
|
|
204
|
+
}
|
|
205
|
+
|
|
57
206
|
#toggleCollapseHandler = () => {
|
|
58
207
|
this.classList.toggle("collapse");
|
|
59
208
|
};
|