llms-py 3.0.14__py3-none-any.whl → 3.0.15__py3-none-any.whl
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.
- llms/extensions/app/ui/threadStore.mjs +10 -3
- llms/extensions/computer_use/README.md +96 -0
- llms/extensions/skills/LICENSE +202 -0
- llms/extensions/skills/__init__.py +130 -0
- llms/extensions/skills/errors.py +25 -0
- llms/extensions/skills/models.py +39 -0
- llms/extensions/skills/parser.py +178 -0
- llms/extensions/skills/ui/index.mjs +335 -0
- llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
- llms/extensions/skills/validator.py +177 -0
- llms/extensions/system_prompts/ui/index.mjs +6 -10
- llms/extensions/tools/ui/index.mjs +1 -1
- llms/main.py +8 -5
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +36 -22
- llms/ui/ctx.mjs +53 -6
- llms/ui/modules/chat/ChatBody.mjs +126 -19
- llms/ui/modules/chat/index.mjs +4 -12
- llms/ui/tailwind.input.css +10 -0
- llms/ui/utils.mjs +25 -1
- {llms_py-3.0.14.dist-info → llms_py-3.0.15.dist-info}/METADATA +1 -1
- {llms_py-3.0.14.dist-info → llms_py-3.0.15.dist-info}/RECORD +26 -17
- {llms_py-3.0.14.dist-info → llms_py-3.0.15.dist-info}/WHEEL +0 -0
- {llms_py-3.0.14.dist-info → llms_py-3.0.15.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.14.dist-info → llms_py-3.0.15.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.14.dist-info → llms_py-3.0.15.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Skill validation logic."""
|
|
2
|
+
|
|
3
|
+
import unicodedata
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .errors import ParseError
|
|
8
|
+
from .parser import find_skill_md, parse_frontmatter
|
|
9
|
+
|
|
10
|
+
MAX_SKILL_NAME_LENGTH = 64
|
|
11
|
+
MAX_DESCRIPTION_LENGTH = 1024
|
|
12
|
+
MAX_COMPATIBILITY_LENGTH = 500
|
|
13
|
+
|
|
14
|
+
# Allowed frontmatter fields per Agent Skills Spec
|
|
15
|
+
ALLOWED_FIELDS = {
|
|
16
|
+
"name",
|
|
17
|
+
"description",
|
|
18
|
+
"license",
|
|
19
|
+
"allowed-tools",
|
|
20
|
+
"metadata",
|
|
21
|
+
"compatibility",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _validate_name(name: str, skill_dir: Path) -> list[str]:
|
|
26
|
+
"""Validate skill name format and directory match.
|
|
27
|
+
|
|
28
|
+
Skill names support i18n characters (Unicode letters) plus hyphens.
|
|
29
|
+
Names must be lowercase and cannot start/end with hyphens.
|
|
30
|
+
"""
|
|
31
|
+
errors = []
|
|
32
|
+
|
|
33
|
+
if not name or not isinstance(name, str) or not name.strip():
|
|
34
|
+
errors.append("Field 'name' must be a non-empty string")
|
|
35
|
+
return errors
|
|
36
|
+
|
|
37
|
+
name = unicodedata.normalize("NFKC", name.strip())
|
|
38
|
+
|
|
39
|
+
if len(name) > MAX_SKILL_NAME_LENGTH:
|
|
40
|
+
errors.append(
|
|
41
|
+
f"Skill name '{name}' exceeds {MAX_SKILL_NAME_LENGTH} character limit "
|
|
42
|
+
f"({len(name)} chars)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if name != name.lower():
|
|
46
|
+
errors.append(f"Skill name '{name}' must be lowercase")
|
|
47
|
+
|
|
48
|
+
if name.startswith("-") or name.endswith("-"):
|
|
49
|
+
errors.append("Skill name cannot start or end with a hyphen")
|
|
50
|
+
|
|
51
|
+
if "--" in name:
|
|
52
|
+
errors.append("Skill name cannot contain consecutive hyphens")
|
|
53
|
+
|
|
54
|
+
if not all(c.isalnum() or c == "-" for c in name):
|
|
55
|
+
errors.append(
|
|
56
|
+
f"Skill name '{name}' contains invalid characters. "
|
|
57
|
+
"Only letters, digits, and hyphens are allowed."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if skill_dir:
|
|
61
|
+
dir_name = unicodedata.normalize("NFKC", skill_dir.name)
|
|
62
|
+
if dir_name != name:
|
|
63
|
+
errors.append(
|
|
64
|
+
f"Directory name '{skill_dir.name}' must match skill name '{name}'"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return errors
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate_description(description: str) -> list[str]:
|
|
71
|
+
"""Validate description format."""
|
|
72
|
+
errors = []
|
|
73
|
+
|
|
74
|
+
if not description or not isinstance(description, str) or not description.strip():
|
|
75
|
+
errors.append("Field 'description' must be a non-empty string")
|
|
76
|
+
return errors
|
|
77
|
+
|
|
78
|
+
if len(description) > MAX_DESCRIPTION_LENGTH:
|
|
79
|
+
errors.append(
|
|
80
|
+
f"Description exceeds {MAX_DESCRIPTION_LENGTH} character limit "
|
|
81
|
+
f"({len(description)} chars)"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return errors
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _validate_compatibility(compatibility: str) -> list[str]:
|
|
88
|
+
"""Validate compatibility format."""
|
|
89
|
+
errors = []
|
|
90
|
+
|
|
91
|
+
if not isinstance(compatibility, str):
|
|
92
|
+
errors.append("Field 'compatibility' must be a string")
|
|
93
|
+
return errors
|
|
94
|
+
|
|
95
|
+
if len(compatibility) > MAX_COMPATIBILITY_LENGTH:
|
|
96
|
+
errors.append(
|
|
97
|
+
f"Compatibility exceeds {MAX_COMPATIBILITY_LENGTH} character limit "
|
|
98
|
+
f"({len(compatibility)} chars)"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return errors
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _validate_metadata_fields(metadata: dict) -> list[str]:
|
|
105
|
+
"""Validate that only allowed fields are present."""
|
|
106
|
+
errors = []
|
|
107
|
+
|
|
108
|
+
extra_fields = set(metadata.keys()) - ALLOWED_FIELDS
|
|
109
|
+
if extra_fields:
|
|
110
|
+
errors.append(
|
|
111
|
+
f"Unexpected fields in frontmatter: {', '.join(sorted(extra_fields))}. "
|
|
112
|
+
f"Only {sorted(ALLOWED_FIELDS)} are allowed."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return errors
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def validate_metadata(metadata: dict, skill_dir: Optional[Path] = None) -> list[str]:
|
|
119
|
+
"""Validate parsed skill metadata.
|
|
120
|
+
|
|
121
|
+
This is the core validation function that works on already-parsed metadata,
|
|
122
|
+
avoiding duplicate file I/O when called from the parser.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
metadata: Parsed YAML frontmatter dictionary
|
|
126
|
+
skill_dir: Optional path to skill directory (for name-directory match check)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of validation error messages. Empty list means valid.
|
|
130
|
+
"""
|
|
131
|
+
errors = []
|
|
132
|
+
errors.extend(_validate_metadata_fields(metadata))
|
|
133
|
+
|
|
134
|
+
if "name" not in metadata:
|
|
135
|
+
errors.append("Missing required field in frontmatter: name")
|
|
136
|
+
else:
|
|
137
|
+
errors.extend(_validate_name(metadata["name"], skill_dir))
|
|
138
|
+
|
|
139
|
+
if "description" not in metadata:
|
|
140
|
+
errors.append("Missing required field in frontmatter: description")
|
|
141
|
+
else:
|
|
142
|
+
errors.extend(_validate_description(metadata["description"]))
|
|
143
|
+
|
|
144
|
+
if "compatibility" in metadata:
|
|
145
|
+
errors.extend(_validate_compatibility(metadata["compatibility"]))
|
|
146
|
+
|
|
147
|
+
return errors
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def validate(skill_dir: Path) -> list[str]:
|
|
151
|
+
"""Validate a skill directory.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
skill_dir: Path to the skill directory
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of validation error messages. Empty list means valid.
|
|
158
|
+
"""
|
|
159
|
+
skill_dir = Path(skill_dir)
|
|
160
|
+
|
|
161
|
+
if not skill_dir.exists():
|
|
162
|
+
return [f"Path does not exist: {skill_dir}"]
|
|
163
|
+
|
|
164
|
+
if not skill_dir.is_dir():
|
|
165
|
+
return [f"Not a directory: {skill_dir}"]
|
|
166
|
+
|
|
167
|
+
skill_md = find_skill_md(skill_dir)
|
|
168
|
+
if skill_md is None:
|
|
169
|
+
return ["Missing required file: SKILL.md"]
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
content = skill_md.read_text()
|
|
173
|
+
metadata, _ = parse_frontmatter(content)
|
|
174
|
+
except ParseError as e:
|
|
175
|
+
return [str(e)]
|
|
176
|
+
|
|
177
|
+
return validate_metadata(metadata, skill_dir)
|
|
@@ -163,7 +163,7 @@ const SystemPromptEditor = {
|
|
|
163
163
|
</div>
|
|
164
164
|
</div>
|
|
165
165
|
<div v-if="hasMessages" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm">
|
|
166
|
-
|
|
166
|
+
<TextViewer prefsName="systemPrompt" :text="$threads.getCurrentThreadSystemPrompt() || 'No System Prompt was used'" />
|
|
167
167
|
</div>
|
|
168
168
|
<div v-else>
|
|
169
169
|
<textarea
|
|
@@ -251,21 +251,17 @@ export default {
|
|
|
251
251
|
}
|
|
252
252
|
})
|
|
253
253
|
|
|
254
|
-
ctx.chatRequestFilters.push(({ request, thread }) => {
|
|
254
|
+
ctx.chatRequestFilters.push(({ request, thread, context }) => {
|
|
255
255
|
|
|
256
|
-
const hasSystemPrompt =
|
|
256
|
+
const hasSystemPrompt = !!context.systemPrompt
|
|
257
|
+
console.log('system_prompts chatRequestFilters', hasSystemPrompt)
|
|
257
258
|
if (hasSystemPrompt) {
|
|
258
259
|
console.log('Already has system prompt', hasSystemPrompt.content)
|
|
259
260
|
return
|
|
260
261
|
}
|
|
261
262
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// add message to start
|
|
265
|
-
request.messages.unshift({
|
|
266
|
-
role: 'system',
|
|
267
|
-
content: ext.prefs.systemPrompt
|
|
268
|
-
})
|
|
263
|
+
if (ext.prefs.systemPrompt) {
|
|
264
|
+
context.systemPrompt = ext.prefs.systemPrompt
|
|
269
265
|
}
|
|
270
266
|
})
|
|
271
267
|
|
llms/main.py
CHANGED
|
@@ -56,7 +56,7 @@ try:
|
|
|
56
56
|
except ImportError:
|
|
57
57
|
HAS_PIL = False
|
|
58
58
|
|
|
59
|
-
VERSION = "3.0.
|
|
59
|
+
VERSION = "3.0.15"
|
|
60
60
|
_ROOT = None
|
|
61
61
|
DEBUG = os.getenv("DEBUG") == "1"
|
|
62
62
|
MOCK = os.getenv("MOCK") == "1"
|
|
@@ -857,7 +857,7 @@ def chat_to_prompt(chat):
|
|
|
857
857
|
prompt = ""
|
|
858
858
|
if "messages" in chat:
|
|
859
859
|
for message in chat["messages"]:
|
|
860
|
-
if message
|
|
860
|
+
if message.get("role") == "user":
|
|
861
861
|
# if content is string
|
|
862
862
|
if isinstance(message["content"], str):
|
|
863
863
|
if prompt:
|
|
@@ -876,7 +876,7 @@ def chat_to_prompt(chat):
|
|
|
876
876
|
def chat_to_system_prompt(chat):
|
|
877
877
|
if "messages" in chat:
|
|
878
878
|
for message in chat["messages"]:
|
|
879
|
-
if message
|
|
879
|
+
if message.get("role") == "system":
|
|
880
880
|
# if content is string
|
|
881
881
|
if isinstance(message["content"], str):
|
|
882
882
|
return message["content"]
|
|
@@ -904,7 +904,7 @@ def last_user_prompt(chat):
|
|
|
904
904
|
prompt = ""
|
|
905
905
|
if "messages" in chat:
|
|
906
906
|
for message in chat["messages"]:
|
|
907
|
-
if message
|
|
907
|
+
if message.get("role") == "user":
|
|
908
908
|
# if content is string
|
|
909
909
|
if isinstance(message["content"], str):
|
|
910
910
|
prompt = message["content"]
|
|
@@ -1807,7 +1807,10 @@ async def g_chat_completion(chat, context=None):
|
|
|
1807
1807
|
# If we get here, all providers failed
|
|
1808
1808
|
if first_exception:
|
|
1809
1809
|
raise first_exception
|
|
1810
|
-
|
|
1810
|
+
|
|
1811
|
+
e = Exception("All providers failed")
|
|
1812
|
+
await g_app.on_chat_error(e, context or {"chat": chat})
|
|
1813
|
+
raise e
|
|
1811
1814
|
|
|
1812
1815
|
|
|
1813
1816
|
async def cli_chat(chat, tools=None, image=None, audio=None, file=None, args=None, raw=False):
|
llms/ui/ai.mjs
CHANGED
llms/ui/app.css
CHANGED
|
@@ -424,9 +424,6 @@
|
|
|
424
424
|
.right-0 {
|
|
425
425
|
right: calc(var(--spacing) * 0);
|
|
426
426
|
}
|
|
427
|
-
.right-1 {
|
|
428
|
-
right: calc(var(--spacing) * 1);
|
|
429
|
-
}
|
|
430
427
|
.right-2 {
|
|
431
428
|
right: calc(var(--spacing) * 2);
|
|
432
429
|
}
|
|
@@ -562,9 +559,6 @@
|
|
|
562
559
|
.-mt-1 {
|
|
563
560
|
margin-top: calc(var(--spacing) * -1);
|
|
564
561
|
}
|
|
565
|
-
.-mt-2 {
|
|
566
|
-
margin-top: calc(var(--spacing) * -2);
|
|
567
|
-
}
|
|
568
562
|
.-mt-8 {
|
|
569
563
|
margin-top: calc(var(--spacing) * -8);
|
|
570
564
|
}
|
|
@@ -631,6 +625,12 @@
|
|
|
631
625
|
.-mb-px {
|
|
632
626
|
margin-bottom: -1px;
|
|
633
627
|
}
|
|
628
|
+
.mb-0 {
|
|
629
|
+
margin-bottom: calc(var(--spacing) * 0);
|
|
630
|
+
}
|
|
631
|
+
.mb-0\.5 {
|
|
632
|
+
margin-bottom: calc(var(--spacing) * 0.5);
|
|
633
|
+
}
|
|
634
634
|
.mb-1 {
|
|
635
635
|
margin-bottom: calc(var(--spacing) * 1);
|
|
636
636
|
}
|
|
@@ -830,15 +830,6 @@
|
|
|
830
830
|
.h-screen {
|
|
831
831
|
height: 100vh;
|
|
832
832
|
}
|
|
833
|
-
.max-h-10 {
|
|
834
|
-
max-height: calc(var(--spacing) * 10);
|
|
835
|
-
}
|
|
836
|
-
.max-h-20 {
|
|
837
|
-
max-height: calc(var(--spacing) * 20);
|
|
838
|
-
}
|
|
839
|
-
.max-h-50 {
|
|
840
|
-
max-height: calc(var(--spacing) * 50);
|
|
841
|
-
}
|
|
842
833
|
.max-h-60 {
|
|
843
834
|
max-height: calc(var(--spacing) * 60);
|
|
844
835
|
}
|
|
@@ -917,6 +908,9 @@
|
|
|
917
908
|
.w-16 {
|
|
918
909
|
width: calc(var(--spacing) * 16);
|
|
919
910
|
}
|
|
911
|
+
.w-28 {
|
|
912
|
+
width: calc(var(--spacing) * 28);
|
|
913
|
+
}
|
|
920
914
|
.w-32 {
|
|
921
915
|
width: calc(var(--spacing) * 32);
|
|
922
916
|
}
|
|
@@ -1685,6 +1679,12 @@
|
|
|
1685
1679
|
background-color: color-mix(in oklab, var(--color-gray-50) 50%, transparent);
|
|
1686
1680
|
}
|
|
1687
1681
|
}
|
|
1682
|
+
.bg-gray-50\/90 {
|
|
1683
|
+
background-color: color-mix(in srgb, oklch(98.5% 0.002 247.839) 90%, transparent);
|
|
1684
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1685
|
+
background-color: color-mix(in oklab, var(--color-gray-50) 90%, transparent);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
1688
|
.bg-gray-100 {
|
|
1689
1689
|
background-color: var(--color-gray-100);
|
|
1690
1690
|
}
|
|
@@ -2524,6 +2524,10 @@
|
|
|
2524
2524
|
--tw-ordinal: ordinal;
|
|
2525
2525
|
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
2526
2526
|
}
|
|
2527
|
+
.tabular-nums {
|
|
2528
|
+
--tw-numeric-spacing: tabular-nums;
|
|
2529
|
+
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
2530
|
+
}
|
|
2527
2531
|
.overline {
|
|
2528
2532
|
text-decoration-line: overline;
|
|
2529
2533
|
}
|
|
@@ -2951,13 +2955,6 @@
|
|
|
2951
2955
|
}
|
|
2952
2956
|
}
|
|
2953
2957
|
}
|
|
2954
|
-
.group-hover\:opacity-50 {
|
|
2955
|
-
&:is(:where(.group):hover *) {
|
|
2956
|
-
@media (hover: hover) {
|
|
2957
|
-
opacity: 50%;
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
2958
|
.group-hover\:opacity-75 {
|
|
2962
2959
|
&:is(:where(.group):hover *) {
|
|
2963
2960
|
@media (hover: hover) {
|
|
@@ -4913,6 +4910,14 @@
|
|
|
4913
4910
|
}
|
|
4914
4911
|
}
|
|
4915
4912
|
}
|
|
4913
|
+
.dark\:bg-gray-800\/90 {
|
|
4914
|
+
&:where(.dark, .dark *) {
|
|
4915
|
+
background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 90%, transparent);
|
|
4916
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
4917
|
+
background-color: color-mix(in oklab, var(--color-gray-800) 90%, transparent);
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4916
4921
|
.dark\:bg-gray-900 {
|
|
4917
4922
|
&:where(.dark, .dark *) {
|
|
4918
4923
|
background-color: var(--color-gray-900);
|
|
@@ -6307,6 +6312,15 @@
|
|
|
6307
6312
|
border-radius: 0 !important;
|
|
6308
6313
|
overflow: auto !important;
|
|
6309
6314
|
}
|
|
6315
|
+
.prose .frontmatter {
|
|
6316
|
+
white-space: pre-wrap;
|
|
6317
|
+
word-break: break-all;
|
|
6318
|
+
margin-bottom: 1em;
|
|
6319
|
+
padding: 0.5rem;
|
|
6320
|
+
font-size: 0.8rem !important;
|
|
6321
|
+
background: #f9fafb;
|
|
6322
|
+
border: 1px solid #e5e7eb;
|
|
6323
|
+
}
|
|
6310
6324
|
.hljs {
|
|
6311
6325
|
background: white;
|
|
6312
6326
|
color: black;
|
llms/ui/ctx.mjs
CHANGED
|
@@ -392,12 +392,11 @@ export class AppContext {
|
|
|
392
392
|
if (Array.isArray(content)) {
|
|
393
393
|
content = content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
394
394
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// }
|
|
395
|
+
if (content && content.startsWith('---')) {
|
|
396
|
+
const headerEnd = content.indexOf('---', 3)
|
|
397
|
+
const header = content.substring(3, headerEnd).trim()
|
|
398
|
+
content = '<div class="frontmatter">' + header + '</div>\n' + content.substring(headerEnd + 3)
|
|
399
|
+
}
|
|
401
400
|
return this.marked.parse(content || '')
|
|
402
401
|
}
|
|
403
402
|
|
|
@@ -409,4 +408,52 @@ export class AppContext {
|
|
|
409
408
|
}
|
|
410
409
|
return this.renderMarkdown(content)
|
|
411
410
|
}
|
|
411
|
+
|
|
412
|
+
createChatContext({ request, thread, context }) {
|
|
413
|
+
if (!request.messages) request.messages = []
|
|
414
|
+
if (!request.metadata) request.metadata = {}
|
|
415
|
+
if (!context) context = {}
|
|
416
|
+
Object.assign(context, {
|
|
417
|
+
systemPrompt: '',
|
|
418
|
+
requiredSystemPrompts: [],
|
|
419
|
+
}, context)
|
|
420
|
+
return {
|
|
421
|
+
request,
|
|
422
|
+
thread,
|
|
423
|
+
context,
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
completeChatContext({ request, thread, context }) {
|
|
428
|
+
|
|
429
|
+
let existingSystemPrompt = request.messages.find(m => m.role === 'system')?.content
|
|
430
|
+
|
|
431
|
+
let existingMessages = request.messages.filter(m => m.role == 'assistant' || m.role == 'tool')
|
|
432
|
+
if (existingMessages.length) {
|
|
433
|
+
const messageTypes = {}
|
|
434
|
+
request.messages.forEach(m => {
|
|
435
|
+
messageTypes[m.role] = (messageTypes[m.role] || 0) + 1
|
|
436
|
+
})
|
|
437
|
+
const summary = JSON.stringify(messageTypes).replace(/"/g, '')
|
|
438
|
+
console.debug(`completeChatContext(${summary})`, request)
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let newSystemPrompts = context.requiredSystemPrompts ?? []
|
|
443
|
+
if (context.systemPrompt) {
|
|
444
|
+
newSystemPrompts.push(context.systemPrompt)
|
|
445
|
+
}
|
|
446
|
+
if (existingSystemPrompt) {
|
|
447
|
+
newSystemPrompts.push(existingSystemPrompt)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let newSystemPrompt = newSystemPrompts.join('\n\n')
|
|
451
|
+
if (newSystemPrompt) {
|
|
452
|
+
// add or replace system prompt
|
|
453
|
+
request.messages = request.messages.filter(m => m.role !== 'system')
|
|
454
|
+
request.messages.unshift({ role: 'system', content: newSystemPrompt })
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
console.debug(`completeChatContext()`, request)
|
|
458
|
+
}
|
|
412
459
|
}
|
|
@@ -56,7 +56,7 @@ function embedHtml(html) {
|
|
|
56
56
|
export const TypeText = {
|
|
57
57
|
template: `
|
|
58
58
|
<div data-type="text" v-if="text.type === 'text'">
|
|
59
|
-
|
|
59
|
+
!<div v-html="html?.trim()" class="whitespace-pre-wrap"></div>
|
|
60
60
|
</div>
|
|
61
61
|
`,
|
|
62
62
|
props: {
|
|
@@ -346,6 +346,124 @@ export const MessageReasoning = {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
export const TextViewer = {
|
|
350
|
+
template: `
|
|
351
|
+
<div v-if="text.length > 200" class="relative group">
|
|
352
|
+
<div class="absolute top-0 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center space-x-2 bg-gray-50/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-md px-2 py-1 z-10 border border-gray-200 dark:border-gray-700 shadow-sm">
|
|
353
|
+
<!-- Style Selector -->
|
|
354
|
+
<div class="relative flex items-center">
|
|
355
|
+
<button type="button" @click="toggleDropdown" class="text-[10px] uppercase font-bold tracking-wider text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none flex items-center select-none">
|
|
356
|
+
<span>{{ prefs || 'pre' }}</span>
|
|
357
|
+
<svg class="mb-0.5 size-3 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M6 9l6 6 6-6"/></svg>
|
|
358
|
+
</button>
|
|
359
|
+
<!-- Popover -->
|
|
360
|
+
<div v-if="dropdownOpen" class="absolute right-0 top-full w-28 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-20 overflow-hidden">
|
|
361
|
+
<button
|
|
362
|
+
v-for="style in textStyles"
|
|
363
|
+
:key="style"
|
|
364
|
+
@click="setStyle(style)"
|
|
365
|
+
class="block w-full text-left px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors uppercase tracking-wider font-medium"
|
|
366
|
+
:class="{ 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20': prefs === style }"
|
|
367
|
+
>
|
|
368
|
+
{{ style }}
|
|
369
|
+
</button>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<div class="w-px h-3 bg-gray-300 dark:bg-gray-600"></div>
|
|
374
|
+
|
|
375
|
+
<!-- Text Length -->
|
|
376
|
+
<span class="text-xs text-gray-500 dark:text-gray-400 tabular-nums" :title="text.length + ' characters'">
|
|
377
|
+
{{ $fmt.humanifyNumber(text.length) }}
|
|
378
|
+
</span>
|
|
379
|
+
|
|
380
|
+
<!-- Maximize Toggle -->
|
|
381
|
+
<button type="button" @click="toggleMaximized" class="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 focus:outline-none p-0.5 rounded transition-colors" :title="isMaximized ? 'Minimize' : 'Maximize'">
|
|
382
|
+
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
383
|
+
<path v-if="isMaximized" fill="currentColor" d="M9 9H3V7h4V3h2zm0 6H3v2h4v4h2zm12 0h-6v6h2v-4h4zm-6-6h6V7h-4V3h-2z"/>
|
|
384
|
+
<path v-else fill="currentColor" d="M3 3h6v2H5v4H3zm0 18h6v-2H5v-4H3zm12 0h6v-6h-2v4h-4zm6-18h-6v2h4v4h2z"/>
|
|
385
|
+
</svg>
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<!-- Content -->
|
|
390
|
+
<div :class="containerClass">
|
|
391
|
+
<div v-if="prefs === 'markdown'" class="prose prose-sm max-w-none dark:prose-invert">
|
|
392
|
+
<div v-html="$fmt.markdown(text)"></div>
|
|
393
|
+
</div>
|
|
394
|
+
<div v-else :class="['p-0.5', contentClass]">{{ text }}</div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
<div v-else class="whitespace-pre-wrap">{{ text }}</div>
|
|
398
|
+
`,
|
|
399
|
+
props: {
|
|
400
|
+
prefsName: String,
|
|
401
|
+
text: String,
|
|
402
|
+
},
|
|
403
|
+
setup(props) {
|
|
404
|
+
const ctx = inject('ctx')
|
|
405
|
+
const textStyles = ['pre', 'normal', 'markdown']
|
|
406
|
+
const prefs = ref('pre')
|
|
407
|
+
const maximized = ref({})
|
|
408
|
+
const dropdownOpen = ref(false)
|
|
409
|
+
const hash = computed(() => ctx.utils.hashString(props.text))
|
|
410
|
+
|
|
411
|
+
const toggleDropdown = () => {
|
|
412
|
+
dropdownOpen.value = !dropdownOpen.value
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const setStyle = (style) => {
|
|
416
|
+
prefs.value = style
|
|
417
|
+
dropdownOpen.value = false
|
|
418
|
+
const key = props.prefsName || 'default'
|
|
419
|
+
const currentPrefs = ctx.getPrefs().textStyle || {}
|
|
420
|
+
ctx.setPrefs({
|
|
421
|
+
textStyle: {
|
|
422
|
+
...currentPrefs,
|
|
423
|
+
[key]: style
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
onMounted(() => {
|
|
429
|
+
const current = ctx.getPrefs()
|
|
430
|
+
const key = props.prefsName || 'default'
|
|
431
|
+
if (current.textStyle && current.textStyle[key]) {
|
|
432
|
+
prefs.value = current.textStyle[key]
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
const isMaximized = computed(() => maximized.value[hash.value])
|
|
437
|
+
|
|
438
|
+
const toggleMaximized = () => {
|
|
439
|
+
maximized.value[hash.value] = !maximized.value[hash.value]
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const containerClass = computed(() => {
|
|
443
|
+
return isMaximized.value ? 'w-full h-full' : 'max-h-60 overflow-y-auto'
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const contentClass = computed(() => {
|
|
447
|
+
if (prefs.value === 'pre') return 'whitespace-pre-wrap font-mono text-xs'
|
|
448
|
+
if (prefs.value === 'normal') return 'font-sans text-sm'
|
|
449
|
+
return ''
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
hash,
|
|
454
|
+
textStyles,
|
|
455
|
+
prefs,
|
|
456
|
+
dropdownOpen,
|
|
457
|
+
toggleDropdown,
|
|
458
|
+
setStyle,
|
|
459
|
+
isMaximized,
|
|
460
|
+
toggleMaximized,
|
|
461
|
+
containerClass,
|
|
462
|
+
contentClass
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
349
467
|
export const ToolArguments = {
|
|
350
468
|
template: `
|
|
351
469
|
<div ref="refArgs" v-if="dict" class="not-prose">
|
|
@@ -356,21 +474,8 @@ export const ToolArguments = {
|
|
|
356
474
|
<td data-arg="html" v-if="$utils.isHtml(v)" style="margin:0;padding:0;width:100%">
|
|
357
475
|
<div v-html="embedHtml(v)" class="w-full h-full"></div>
|
|
358
476
|
</td>
|
|
359
|
-
<td data-arg="string" v-else-if="typeof v === 'string'" class="align-top py-2 px-4 text-sm
|
|
360
|
-
<
|
|
361
|
-
<button type="button" @click="maximized[k] = !maximized[k]" class="absolute top-0 right-3 opacity-0 group-hover:opacity-50 transition-opacity duration-200 rounded focus:outline-none focus:ring-0">
|
|
362
|
-
<div class="flex items-center space-x-1">
|
|
363
|
-
<span class="text-xs text-gray-600 dark:text-gray-400" :title="v.length + ' characters'">{{ $fmt.humanifyNumber(v.length) }}</span>
|
|
364
|
-
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
365
|
-
<path v-if="maximized[k]" fill="currentColor" d="M9 9H3V7h4V3h2zm0 6H3v2h4v4h2zm12 0h-6v6h2v-4h4zm-6-6h6V7h-4V3h-2z"/>
|
|
366
|
-
<path v-else fill="currentColor" d="M3 3h6v2H5v4H3zm0 18h6v-2H5v-4H3zm12 0h6v-6h-2v4h-4zm6-18h-6v2h4v4h2z"/>
|
|
367
|
-
</svg>
|
|
368
|
-
</div>
|
|
369
|
-
</button>
|
|
370
|
-
<div v-if="!maximized[k]" class="max-h-60 overflow-y-auto">{{ v }}</div>
|
|
371
|
-
<div v-else class="w-full h-full">{{ v }}</div>
|
|
372
|
-
</div>
|
|
373
|
-
<div v-else>{{ v }}</div>
|
|
477
|
+
<td data-arg="string" v-else-if="typeof v === 'string'" class="align-top py-2 px-4 text-sm">
|
|
478
|
+
<TextViewer prefsName="toolArgs" :text="v" />
|
|
374
479
|
</td>
|
|
375
480
|
<td data-arg="value" v-else class="align-top py-2 px-4 text-sm whitespace-pre-wrap">
|
|
376
481
|
<HtmlFormat :value="v" :classes="$utils.htmlFormatClasses" />
|
|
@@ -457,9 +562,11 @@ export const ToolOutput = {
|
|
|
457
562
|
</span>
|
|
458
563
|
</div>
|
|
459
564
|
</div>
|
|
460
|
-
<div class="
|
|
461
|
-
<
|
|
462
|
-
|
|
565
|
+
<div class="px-3 py-2">
|
|
566
|
+
<div v-if="$ctx.prefs.toolFormat !== 'preview' || !hasJsonStructure(output.content)">
|
|
567
|
+
<TextViewer prefsName="toolOutput" :text="output.content" />
|
|
568
|
+
</div>
|
|
569
|
+
<div v-else class="not-prose text-xs">
|
|
463
570
|
<HtmlFormat v-if="tryParseJson(output.content)" :value="tryParseJson(output.content)" :classes="$utils.htmlFormatClasses" />
|
|
464
571
|
<div v-else class="text-gray-500 italic p-2">Invalid JSON content</div>
|
|
465
572
|
</div>
|