Skip to content

Commit 54bee0f

Browse files
authored
πŸ› Bugfix: skill names and descriptions never load to context (#3207)
πŸ› Bugfix: official skills not copied to target directory
1 parent a57538f commit 54bee0f

7 files changed

Lines changed: 176 additions & 83 deletions

File tree

β€Žbackend/agents/create_agent_info.pyβ€Ž

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -433,25 +433,6 @@ async def create_agent_config(
433433
}
434434
system_prompt = Template(prompt_template["system_prompt"], undefined=StrictUndefined).render(render_kwargs)
435435

436-
context_components = build_context_components(
437-
duty=duty_prompt,
438-
constraint=constraint_prompt,
439-
few_shots=few_shots_prompt,
440-
app_name=app_name,
441-
app_description=app_description,
442-
time_str=time_str,
443-
user_id=user_id,
444-
language=language,
445-
is_manager=is_manager,
446-
tools=render_kwargs["tools"],
447-
skills=skills,
448-
managed_agents=render_kwargs["managed_agents"],
449-
external_a2a_agents=render_kwargs["external_a2a_agents"],
450-
memory_list=memory_list,
451-
memory_search_query=last_user_query,
452-
knowledge_base_summary=knowledge_base_summary,
453-
)
454-
455436
model_id_to_use = override_model_id if override_model_id else agent_info.get("model_id")
456437
model_max_tokens = 10000
457438
if model_id_to_use is not None:
@@ -461,8 +442,33 @@ async def create_agent_config(
461442
model_max_tokens = model_info["max_tokens"]
462443
else:
463444
model_name = "main_model"
464-
# Use agent-level setting for context management, default to False
445+
446+
# Use agent-level setting for context management, default to False.
447+
# When ContextManager is disabled, do not attach context_components because
448+
# downstream runtime may prefer component-based prompt assembly over the
449+
# rendered system_prompt, causing the actual model input to diverge from the
450+
# template output.
465451
enable_context_manager = agent_info.get("enable_context_manager", False)
452+
context_components = []
453+
if enable_context_manager:
454+
context_components = build_context_components(
455+
duty=duty_prompt,
456+
constraint=constraint_prompt,
457+
few_shots=few_shots_prompt,
458+
app_name=app_name,
459+
app_description=app_description,
460+
time_str=time_str,
461+
user_id=user_id,
462+
language=language,
463+
is_manager=is_manager,
464+
tools=render_kwargs["tools"],
465+
skills=skills,
466+
managed_agents=render_kwargs["managed_agents"],
467+
external_a2a_agents=render_kwargs["external_a2a_agents"],
468+
memory_list=memory_list,
469+
memory_search_query=last_user_query,
470+
knowledge_base_summary=knowledge_base_summary,
471+
)
466472
cm_config = ContextManagerConfig(
467473
enabled=enable_context_manager,
468474
token_threshold=model_max_tokens,

β€Ždocker/deploy.shβ€Ž

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,15 @@ prepare_directory_and_data() {
796796
create_dir_with_permission "$NEXENT_USER_DIR" 775
797797
echo " πŸ–₯️ Nexent user workspace: $NEXENT_USER_DIR"
798798

799+
# Copy official-skills-zip folder to /mnt/nexent
800+
if [ -d "official-skills-zip" ]; then
801+
cp -rn official-skills-zip "$NEXENT_USER_DIR/"
802+
chmod -R 775 "$NEXENT_USER_DIR/official-skills-zip"
803+
echo " πŸ“¦ Official skills copied to $NEXENT_USER_DIR/official-skills-zip"
804+
else
805+
echo " ⚠️ official-skills-zip directory not found, skipping skills copy"
806+
fi
807+
799808
# Export for docker-compose
800809
export NEXENT_USER_DIR
801810

@@ -1358,7 +1367,7 @@ main_deploy() {
13581367
echo "--------------------------------"
13591368
echo ""
13601369

1361-
APP_VERSION="$(get_app_version)"
1370+
APP_VERSION="latest"
13621371
if [ -z "$APP_VERSION" ]; then
13631372
echo "❌ Failed to get app version, please check the backend/consts/const.py file"
13641373
exit 1

β€Žsdk/nexent/core/agents/agent_model.pyβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,11 @@ def to_messages(self) -> List[Dict[str, str]]:
311311
return [{"role": "system", "content": self.formatted_description}]
312312
return []
313313

314-
def add_skill(self, name: str, description: str, examples: List[str] = None) -> None:
314+
def add_skill(self, name: str, description: str) -> None:
315315
"""Add a skill definition."""
316316
self.skills.append({
317317
"name": name,
318-
"description": description,
319-
"examples": examples or []
318+
"description": description
320319
})
321320

322321

β€Žtest/backend/agents/test_create_agent_info.pyβ€Ž

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3835,6 +3835,52 @@ async def test_join_minio_file_description_to_query_current_files_priority(self)
38353835
pos_history = result.find("history_2.pdf")
38363836
assert pos_current < pos_history, "Current message files should appear before history files"
38373837

3838+
def test_format_minio_files_for_content_formats_presigned_urls(self):
3839+
"""History attachment formatting should include both internal and external URLs."""
3840+
result = _format_minio_files_for_content(
3841+
[
3842+
{
3843+
"name": "report.pdf",
3844+
"object_name": "tenant-a/report.pdf",
3845+
"presigned_url": "https://signed.example/report.pdf",
3846+
}
3847+
]
3848+
)
3849+
3850+
assert result.startswith("\n[Attached files]:\n")
3851+
assert "report.pdf" in result
3852+
assert "s3://" in result
3853+
assert "presigned_url: https://signed.example/report.pdf" in result
3854+
3855+
def test_convert_history_with_minio_files_embeds_file_info(self):
3856+
"""History items should preserve text and append formatted attachment details."""
3857+
history = [
3858+
HistoryItem(
3859+
role="user",
3860+
content="Please review this file",
3861+
minio_files=[
3862+
{
3863+
"name": "notes.txt",
3864+
"object_name": "tenant-a/notes.txt",
3865+
}
3866+
],
3867+
),
3868+
HistoryItem(role="assistant", content="Done", minio_files=None),
3869+
]
3870+
3871+
result = _convert_history_with_minio_files(history)
3872+
3873+
assert len(result) == 2
3874+
assert result[0].role == "user"
3875+
assert result[0].content.startswith("Please review this file")
3876+
assert "[Attached files]:" in result[0].content
3877+
assert "notes.txt" in result[0].content
3878+
assert result[1].content == "Done"
3879+
3880+
def test_convert_history_with_minio_files_returns_none_for_none(self):
3881+
"""None history should remain None for downstream SDK compatibility."""
3882+
assert _convert_history_with_minio_files(None) is None
3883+
38383884

38393885
class TestPreparePromptTemplates:
38403886
"""Tests for the prepare_prompt_templates function"""
@@ -3865,6 +3911,22 @@ async def test_prepare_prompt_templates_worker_en(self):
38653911
assert result["system_prompt"] == "test system prompt"
38663912
assert result["test"] == "template"
38673913

3914+
@pytest.mark.asyncio
3915+
async def test_prepare_prompt_templates_overwrites_existing_system_prompt(self):
3916+
"""Latest rendered system prompt should replace the template default."""
3917+
with patch('backend.agents.create_agent_info.get_agent_prompt_template') as mock_get_template:
3918+
mock_get_template.return_value = {
3919+
"system_prompt": "stale prompt",
3920+
"user_prompt": "keep me",
3921+
}
3922+
3923+
result = await prepare_prompt_templates(False, "fresh system prompt", "en")
3924+
3925+
assert result == {
3926+
"system_prompt": "fresh system prompt",
3927+
"user_prompt": "keep me",
3928+
}
3929+
38683930

38693931
class TestExtractUrlFromCard:
38703932
"""Tests for the _extract_url_from_card function"""

β€Žtest/backend/test_document_vector_integration.pyβ€Ž

Lines changed: 57 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""
2-
Integration test for document vector operations
2+
Integration test for document vector operations.
33
4-
This test demonstrates the complete workflow from ES retrieval to clustering.
5-
Note: This requires a running Elasticsearch instance.
4+
This module validates the embedding and clustering workflow using deterministic
5+
fixtures so the clustering assertions stay stable across environments.
66
"""
77
import os
88
import sys
@@ -80,82 +80,84 @@
8080

8181

8282
class TestDocumentVectorIntegration:
83-
"""Integration tests for document vector operations"""
84-
83+
"""Integration tests for document vector operations."""
84+
8585
def test_complete_workflow(self):
86-
"""Test complete workflow: embedding calculation -> clustering"""
87-
# Simulate document chunks with embeddings
86+
"""Test complete workflow: embedding calculation -> clustering."""
8887
chunks_1 = [
89-
{'embedding': np.random.rand(128).tolist(), 'content': 'Content for doc 1 chunk 1'},
90-
{'embedding': np.random.rand(128).tolist(), 'content': 'Content for doc 1 chunk 2'},
91-
{'embedding': np.random.rand(128).tolist(), 'content': 'Content for doc 1 chunk 3'}
88+
{"embedding": [1.0, 0.0], "content": "Document one chunk A"},
89+
{"embedding": [0.9, 0.1], "content": "Document one chunk B"},
90+
{"embedding": [0.95, 0.05], "content": "Document one chunk C"},
9291
]
93-
9492
chunks_2 = [
95-
{'embedding': np.random.rand(128).tolist(), 'content': 'Content for doc 2 chunk 1'},
96-
{'embedding': np.random.rand(128).tolist(), 'content': 'Content for doc 2 chunk 2'}
93+
{"embedding": [0.0, 1.0], "content": "Document two chunk A"},
94+
{"embedding": [0.1, 0.9], "content": "Document two chunk B"},
9795
]
98-
9996
chunks_3 = [
100-
{'embedding': np.random.rand(128).tolist(), 'content': 'Content for doc 3 chunk 1'},
101-
{'embedding': np.random.rand(128).tolist(), 'content': 'Content for doc 3 chunk 2'},
102-
{'embedding': np.random.rand(128).tolist(), 'content': 'Content for doc 3 chunk 3'},
103-
{'embedding': np.random.rand(128).tolist(), 'content': 'Content for doc 3 chunk 4'}
97+
{"embedding": [0.85, 0.15], "content": "Document three chunk A"},
98+
{"embedding": [0.8, 0.2], "content": "Document three chunk B"},
99+
{"embedding": [0.88, 0.12], "content": "Document three chunk C"},
100+
{"embedding": [0.83, 0.17], "content": "Document three chunk D"},
104101
]
105-
106-
# Calculate document embeddings
102+
107103
doc_embedding_1 = calculate_document_embedding(chunks_1, use_weighted=True)
108104
doc_embedding_2 = calculate_document_embedding(chunks_2, use_weighted=True)
109105
doc_embedding_3 = calculate_document_embedding(chunks_3, use_weighted=True)
110-
106+
111107
assert doc_embedding_1 is not None
112108
assert doc_embedding_2 is not None
113109
assert doc_embedding_3 is not None
114-
115-
# Create document embeddings dictionary
110+
116111
doc_embeddings = {
117-
'doc_001': doc_embedding_1,
118-
'doc_002': doc_embedding_2,
119-
'doc_003': doc_embedding_3
112+
"doc_001": doc_embedding_1,
113+
"doc_002": doc_embedding_2,
114+
"doc_003": doc_embedding_3,
120115
}
121-
122-
# Determine optimal K
116+
123117
embeddings_array = np.array([doc_embedding_1, doc_embedding_2, doc_embedding_3])
124118
optimal_k = auto_determine_k(embeddings_array, min_k=2, max_k=3)
125-
126-
assert 2 <= optimal_k <= 3
127-
128-
# Perform clustering
119+
120+
assert optimal_k == 2
121+
129122
clusters = kmeans_cluster_documents(doc_embeddings, k=optimal_k)
130-
123+
131124
assert len(clusters) == optimal_k
132125
assert sum(len(docs) for docs in clusters.values()) == 3
133-
126+
assert sorted(len(docs) for docs in clusters.values()) == [1, 2]
127+
128+
cluster_sets = [set(docs) for docs in clusters.values()]
129+
assert {"doc_001", "doc_003"} in cluster_sets
130+
assert {"doc_002"} in cluster_sets
131+
134132
def test_large_dataset_clustering(self):
135-
"""Test clustering with larger simulated dataset"""
136-
# Create simulated document embeddings
137-
n_docs = 50
138-
doc_embeddings = {
139-
f'doc_{i:03d}': np.random.rand(128) for i in range(n_docs)
133+
"""Test clustering with a deterministic larger simulated dataset."""
134+
cluster_a = {
135+
f"doc_a_{i:03d}": np.array([1.0 + i * 0.002, 1.0 + i * 0.001, 0.2])
136+
for i in range(20)
137+
}
138+
cluster_b = {
139+
f"doc_b_{i:03d}": np.array([5.0 + i * 0.002, 5.0 + i * 0.001, 0.4])
140+
for i in range(15)
141+
}
142+
cluster_c = {
143+
f"doc_c_{i:03d}": np.array([9.0 + i * 0.002, 1.0 + i * 0.001, 0.6])
144+
for i in range(15)
140145
}
141-
142-
# Auto-determine K
146+
doc_embeddings = {**cluster_a, **cluster_b, **cluster_c}
147+
n_docs = len(doc_embeddings)
148+
143149
embeddings_array = np.array(list(doc_embeddings.values()))
144-
optimal_k = auto_determine_k(embeddings_array, min_k=3, max_k=15)
145-
146-
assert 3 <= optimal_k <= 15
147-
148-
# Cluster documents
149-
clusters = kmeans_cluster_documents(doc_embeddings, k=optimal_k)
150-
151-
assert len(clusters) == optimal_k
150+
optimal_k = auto_determine_k(embeddings_array, min_k=3, max_k=6)
151+
152+
assert 3 <= optimal_k <= 6
153+
154+
clusters = kmeans_cluster_documents(doc_embeddings, k=3)
155+
156+
assert len(clusters) == 3
152157
assert sum(len(docs) for docs in clusters.values()) == n_docs
153-
154-
# Verify cluster sizes are reasonable
155-
cluster_sizes = [len(docs) for docs in clusters.values()]
156-
assert min(cluster_sizes) >= 1
157-
# Allow for some imbalance in clustering results (realistic for random data)
158-
assert max(cluster_sizes) <= n_docs * 0.7 # No single cluster dominates too much
158+
159+
cluster_sizes = sorted(len(docs) for docs in clusters.values())
160+
assert cluster_sizes == [15, 15, 20]
159161

160162

161163
if __name__ == '__main__':

β€Žtest/sdk/core/agents/test_agent_model.pyβ€Ž

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,11 @@ def test_model_config_creation_with_all_fields(self):
302302
temperature=0.7,
303303
top_p=0.9,
304304
ssl_verify=False,
305-
model_factory="openai"
305+
model_factory="openai",
306+
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
307+
max_tokens=4096,
308+
timeout_seconds=45.5,
309+
concurrency_limit=3,
306310
)
307311
assert config.cite_name == "gpt-4"
308312
assert config.api_key == "sk-test-key"
@@ -312,6 +316,10 @@ def test_model_config_creation_with_all_fields(self):
312316
assert config.top_p == 0.9
313317
assert config.ssl_verify is False
314318
assert config.model_factory == "openai"
319+
assert config.extra_body == {"chat_template_kwargs": {"enable_thinking": False}}
320+
assert config.max_tokens == 4096
321+
assert config.timeout_seconds == 45.5
322+
assert config.concurrency_limit == 3
315323

316324
def test_model_config_creation_with_minimal_fields(self):
317325
"""Test ModelConfig creation with only required fields."""
@@ -326,6 +334,10 @@ def test_model_config_creation_with_minimal_fields(self):
326334
assert config.top_p == 0.95
327335
assert config.ssl_verify is True
328336
assert config.model_factory is None
337+
assert config.extra_body is None
338+
assert config.max_tokens is None
339+
assert config.timeout_seconds is None
340+
assert config.concurrency_limit is None
329341

330342
def test_model_config_defaults(self):
331343
"""Test ModelConfig has correct default values."""

β€Žtest/sdk/core/agents/test_context_component.pyβ€Ž

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,15 +418,18 @@ def test_to_messages_empty(self):
418418

419419
def test_add_skill(self):
420420
comp = agent_model_module.SkillsComponent()
421-
comp.add_skill("python_coding", "Write Python code", ["example1", "example2"])
421+
comp.add_skill("python_coding", "Write Python code")
422422
assert len(comp.skills) == 1
423423
assert comp.skills[0]["name"] == "python_coding"
424-
assert comp.skills[0]["examples"] == ["example1", "example2"]
424+
assert comp.skills[0]["description"] == "Write Python code"
425425

426426
def test_add_skill_without_examples(self):
427427
comp = agent_model_module.SkillsComponent()
428428
comp.add_skill("skill_name", "skill desc")
429-
assert comp.skills[0]["examples"] == []
429+
assert comp.skills[0] == {
430+
"name": "skill_name",
431+
"description": "skill desc",
432+
}
430433

431434

432435
class TestMemoryComponent:

0 commit comments

Comments
Β (0)