Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!--
<!--
AUTOMATED AGENT GREETING

Greetings, netizen! We welcome your pull requests to Skillware.
Greetings, netizen! We welcome your pull requests to Skillware.
Before submitting your code, please ensure you have:
1. Fully read and understood the linked GitHub Issue and its requirements.
2. Analyzed the core architecture (`loader.py`, `base_skill.py`) to ensure your approach aligns with the framework natively.
Expand All @@ -13,7 +13,7 @@ We are excited to review your capabilities! Let's build together.

## Description

<!--
<!--
Agents: Please summarize the logic, cognition, and governance changes introduced in this PR.
Humans: Please describe what this PR does and why it's needed.
-->
Expand All @@ -36,8 +36,8 @@ Humans: Please describe what this PR does and why it's needed.

## Constitution & Safety (If adding/modifying a Skill)

<!--
State the constitutional boundaries applied to this skill to ensure safe execution.
<!--
State the constitutional boundaries applied to this skill to ensure safe execution.
Example: "This skill only performs read operations on the blockchain and does not sign transactions."
-->

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ This is the most critical file. It is the "driver" for the LLM.
* **Feature**: Use `[Framework Feature]` for changes to the core engine.
* **Bug**: Use `[Bug Report]` for errors.
* **RFC**: Use `[Request for Comments]` for major architectural discussions.

[Open a New Issue Here](https://github.com/ARPAHLS/skillware/issues/new/choose) is the first step.
*Wait for approval/feedback before writing code.*
2. **Fork** the repository.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ print(response.text)

We are building the "App Store" for Agents and require professional, robust, and safe skills.

We actively encourage both humans and autonomous agents to contribute to this repository!
We actively encourage both humans and autonomous agents to contribute to this repository!

* Please read our **[Agent Code of Conduct](CODE_OF_CONDUCT.md)** which outlines our strict expectations for deterministic outputs, zero LLM code generation, and safety boundaries.
* When submitting skills, our new **Agent-Friendly Pull Request Template** provides a checklist to ensure your logic aligns natively with `loader.py` and `base_skill.py`.
Expand Down
4 changes: 2 additions & 2 deletions docs/usage/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ Claude stops generating when it wants to call a tool. You must execute and reply
```python
if message.stop_reason == "tool_use":
tool_use = next(b for b in message.content if b.type == "tool_use")

# 1. Execute
print(f"Calling {tool_use.name}...")
result = my_skill.execute(tool_use.input)

# 2. Reply with Result
response = client.messages.create(
model="claude-3-opus-20240229",
Expand Down
6 changes: 3 additions & 3 deletions docs/usage/gemini.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ response = chat.send_message("Scan wallet...")
for part in response.parts:
if fn := part.function_call:
print(f"Model wants to call {fn.name} with {fn.args}")

# 1. Execute Logic
result = my_skill.execute(dict(fn.args))

# 2. Send Result
chat.send_message(
genai.prototypes.Part(
Expand All @@ -78,7 +78,7 @@ sys_prompt = "You are a very helpful assistant serving a bank..."

# Use python logic offline before starting the chat session
optimized_ctx_result = rewriter['module'].PromptRewriter().execute({
"raw_text": sys_prompt,
"raw_text": sys_prompt,
"compression_aggression": "high"
})

Expand Down
12 changes: 6 additions & 6 deletions docs/usage/ollama.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Skillware natively supports [Ollama](https://ollama.com/), enabling you to run o

Here is a simple example demonstrating how to load a skill and execute it using a local model running via Ollama.

```python
````python
import json
import re
import ollama
Expand Down Expand Up @@ -78,20 +78,20 @@ if tool_match:
tool_call = json.loads(tool_match.group(1))
fn_name = tool_call.get("tool")
fn_args = tool_call.get("arguments", {})

if fn_name == "finance/wallet_screening":
print(f"⚙️ Executing skill '{fn_name}' locally...")
api_result = wallet_skill.execute(fn_args)

# Give result back to model
messages.append({"role": "assistant", "content": message_content})
messages.append({
"role": "user",
"role": "user",
"content": f"SYSTEM RESPONSE (Result from {fn_name}):\n```json\n{json.dumps(api_result)}\n```\nPlease continue."
})

print("\n🤖 Sending tool results back to Agent...")
final_resp = ollama.chat(model=model_name, messages=messages)
print("\n💬 Final Answer:")
print(final_resp.get("message", {}).get("content", ""))
```
````
45 changes: 29 additions & 16 deletions examples/ollama_skills_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import json
import re
import ollama
Expand All @@ -9,6 +8,7 @@
# Load Env for API Keys if any needed by skills
load_env_file()


def load_and_initialize_skill(path):
bundle = SkillLoader.load_skill(path)
skill_class = None
Expand All @@ -21,6 +21,7 @@ def load_and_initialize_skill(path):
raise ValueError(f"Could not find a valid Skill class in {path}")
return bundle, skill_class()


# 1. Load the 3 Skills dynamically
SKILL_PATHS = [
"finance/wallet_screening",
Expand All @@ -36,17 +37,18 @@ def load_and_initialize_skill(path):
bundle, skill_instance = load_and_initialize_skill(path)
name = bundle["manifest"]["name"]
skills_registry[name] = skill_instance

# Use the prompt adapter for Ollama
tool_text = SkillLoader.to_ollama_prompt(bundle)
tool_text += f"\n**Cognitive Instructions:**\n{bundle.get('instructions', '')}\n"
tool_descriptions.append(tool_text)

print(f"Loaded Skill: {name}")

# 2. Build the System Prompt tailored for text-based tool calling
combined_system_prompt = """You are an intelligent agent equipped with specialized capabilities (skills).
To use a skill, you MUST output a JSON code block in the EXACT following format and then STOP GENERATING. Do not add conversational text after the JSON block.
To use a skill, you MUST output a JSON code block in the EXACT following format and then STOP GENERATING.
Do not add conversational text after the JSON block.

```json
{
Expand All @@ -57,14 +59,18 @@ def load_and_initialize_skill(path):
}
```

Wait until you receive the SYSTEM RESPONSE containing the tool execution results before proceeding. Once you have the results, provide your final answer to the user.
Wait until you receive the SYSTEM RESPONSE containing the tool execution results before proceeding.
Once you have the results, provide your final answer to the user.

Here are the available skills and their instructions:
""" + "\n---\n".join(tool_descriptions)

# 3. Setup Ollama Chat
model_name = "llama3"
user_query = "Please screen this ethereum wallet: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045. Also, please rewrite this prompt for me: 'make me a cool image of a cat'."
user_query = (
"Please screen this ethereum wallet: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045. "
"Also, please rewrite this prompt for me: 'make me a cool image of a cat'."
)

print(f"\nUser: {user_query}")

Expand All @@ -76,42 +82,46 @@ def load_and_initialize_skill(path):
print(f"\n🤖 Calling Ollama model: {model_name}...")

# 4. Handle Conversation & Tool Parsing Loop
for _ in range(5): # Max steps to prevent infinite loops
for _ in range(5): # Max steps to prevent infinite loops
response = ollama.chat(
model=model_name,
messages=messages
)

message_content = response.get("message", {}).get("content", "")
print(f"\n[Model Output]:\n{message_content}")
messages.append({"role": "assistant", "content": message_content})

# Try to parse a tool call inside ```json ... ```
tool_match = re.search(r"```json\s*({.*?})\s*```", message_content, re.DOTALL)

if tool_match:
try:
tool_call = json.loads(tool_match.group(1))
fn_name = tool_call.get("tool")
fn_args = tool_call.get("arguments", {})

print(f"\n🤖 Agent invoked tool: {fn_name}")
print(f" Arguments: {fn_args}")

if fn_name in skills_registry:
print(f"⚙️ Executing skill '{fn_name}' locally...")
try:
api_result = skills_registry[fn_name].execute(fn_args)
result_str = json.dumps(api_result)
except Exception as e:
result_str = f"Error executing tool: {e}"

print(f"📤 Result generated ({len(result_str)} bytes)")

# Send the result back to the model masquerading as a system/user update
messages.append({
"role": "user",
"content": f"SYSTEM RESPONSE (Result from {fn_name}):\n```json\n{result_str}\n```\nPlease continue based on this result."
"content": (
f"SYSTEM RESPONSE (Result from {fn_name}):\n"
f"```json\n{result_str}\n```\n"
"Please continue based on this result."
)
})
else:
print(f"Unknown function requested: {fn_name}")
Expand All @@ -121,7 +131,10 @@ def load_and_initialize_skill(path):
})
except json.JSONDecodeError:
print("Failed to decode JSON from tool call block.")
messages.append({"role": "user", "content": "SYSTEM ERROR: Invalid JSON format. Please output valid JSON."})
messages.append({
"role": "user",
"content": "SYSTEM ERROR: Invalid JSON format. Please output valid JSON."
})
else:
# If no tool block was found, assume the agent is done and providing final answer
print("\n💬 Final Answer reached. End of execution.")
Expand Down
Binary file added flake8_report.txt
Binary file not shown.
2 changes: 1 addition & 1 deletion skills/optimization/prompt_rewriter/instructions.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cognition Instructions: Prompt Rewriter Middleware

You have access to the `optimization/prompt_rewriter` tool.
You have access to the `optimization/prompt_rewriter` tool.
This tool is crucial for saving context window budget and drastically lowering operational costs during extensive loops.

## When to use this skill
Expand Down
6 changes: 3 additions & 3 deletions skillware/core/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,15 @@ def to_ollama_prompt(skill_bundle: Dict[str, Any]) -> str:
prompt = f"### Tool: `{name}`\n"
prompt += f"**Description:** {description}\n"
prompt += "**Parameters:**\n"

props = parameters.get("properties", {})
required = parameters.get("required", [])

if not props:
prompt += "- None\n"
else:
for k, v in props.items():
req_str = "Required" if k in required else "Optional"
prompt += f"- `{k}` ({v.get('type', 'any')}): {v.get('description', '')} [{req_str}]\n"

return prompt
35 changes: 22 additions & 13 deletions tests/skills/finance/test_wallet_screening.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
import pytest
import os
from unittest.mock import patch, MagicMock
from skillware.core.loader import SkillLoader


def get_skill():
bundle = SkillLoader.load_skill("finance/wallet_screening")
# Initialize without needing real API keys
return bundle['module'].WalletScreeningSkill()


@patch("skills.finance.wallet_screening.skill.requests.get")
def test_wallet_screening_success(mock_get):
skill = get_skill()
skill.etherscan_api_key = "dummy_key"

# Mock responses
mock_eth_balance = MagicMock()
mock_eth_balance.json.return_value = {"status": "1", "result": "1000000000000000000"} # 1 ETH
mock_eth_balance.json.return_value = {"status": "1", "result": "1000000000000000000"} # 1 ETH

mock_txs = MagicMock()
mock_txs.json.return_value = {"status": "1", "result": [
{"from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".lower(), "to": "0x123", "value": "500000000000000000", "isError": "0", "gasUsed": "21000", "gasPrice": "1000000000"}
{
"from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".lower(),
"to": "0x123",
"value": "500000000000000000",
"isError": "0",
"gasUsed": "21000",
"gasPrice": "1000000000"
}
]}

mock_price = MagicMock()
mock_price.json.return_value = {"ethereum": {"usd": 2000.0, "eur": 1800.0}}

# Configure mock side_effect based on URL/params
def get_side_effect(url, **kwargs):
if "action" in kwargs.get("params", {}):
Expand All @@ -33,24 +40,26 @@ def get_side_effect(url, **kwargs):
elif kwargs["params"]["action"] == "txlist":
return mock_txs
return mock_price

mock_get.side_effect = get_side_effect

result = skill.execute({"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"})

assert "error" not in result
assert "summary" in result
assert result["summary"]["balance_eth"] == 1.0
assert result["summary"]["balance_usd"] == 2000.0
assert "financial_analysis" in result
assert result["financial_analysis"]["value_out_eth"] == 0.5
assert result["financial_analysis"]["value_out_eth"] == 0.5


def test_wallet_screening_invalid_address():
skill = get_skill()
result = skill.execute({"address": "invalid_addr"})
assert "error" in result
assert "Invalid Ethereum address" in result["error"]



def test_wallet_screening_missing_key():
skill = get_skill()
skill.etherscan_api_key = None
Expand Down
6 changes: 5 additions & 1 deletion tests/test_loader.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import pytest
from skillware.core.loader import SkillLoader


def test_load_skill_not_found():
with pytest.raises(FileNotFoundError):
SkillLoader.load_skill("nonexistent_skill_path_12345")


def test_to_ollama_prompt():
dummy_bundle = {
"manifest": {
Expand All @@ -19,12 +21,13 @@ def test_to_ollama_prompt():
}
}
}

prompt = SkillLoader.to_ollama_prompt(dummy_bundle)
assert "### Tool: `test_ollama_skill`" in prompt
assert "**Description:** A very useful test skill." in prompt
assert "- `arg1` (string): The first arg [Required]" in prompt


def test_to_gemini_tool():
dummy_bundle = {
"manifest": {
Expand All @@ -43,6 +46,7 @@ def test_to_gemini_tool():
assert tool["parameters"]["type"] == "OBJECT"
assert tool["parameters"]["properties"]["param1"]["type"] == "STRING"


def test_to_claude_tool():
dummy_bundle = {
"manifest": {
Expand Down
Loading