Skip to main content
Glama
test_story_workflow.py13.5 kB
import os import pytest import httpx from typing import Any, Dict, List # Import MCP tools from markdown_editor.tools.file_ops import create_file, list_directory, delete_item from markdown_editor.tools.edit_tools import ( get_document_structure, read_element, replace_content, insert_element, delete_element, undo_changes, search_in_document, get_element_context, move_document_element, update_document_metadata, ) from markdown_editor.core.path_utils import PathResolver OLLAMA_URL = "http://127.0.0.1:11434" MODEL = "qwen3:8b" # Define tools schema (same as before) TOOLS_SCHEMA = [ { "type": "function", "function": { "name": "search_tools", "description": "Find the right tool for your task.", "parameters": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"], }, }, }, { "type": "function", "function": { "name": "list_directory", "description": "List files.", "parameters": { "type": "object", "properties": {"path": {"type": "string"}}, "required": [], }, }, }, { "type": "function", "function": { "name": "create_file", "description": "Create file.", "parameters": { "type": "object", "properties": { "path": {"type": "string"}, "content": {"type": "string"}, }, "required": ["path"], }, }, }, { "type": "function", "function": { "name": "delete_item", "description": "Delete file/dir.", "parameters": { "type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"], }, }, }, { "type": "function", "function": { "name": "get_document_structure", "description": "Get MD structure.", "parameters": { "type": "object", "properties": {"file_path": {"type": "string"}}, "required": ["file_path"], }, }, }, { "type": "function", "function": { "name": "read_element", "description": "Read element.", "parameters": { "type": "object", "properties": { "file_path": {"type": "string"}, "path": {"type": "string"}, }, "required": ["file_path", "path"], }, }, }, { "type": "function", "function": { "name": "replace_content", "description": "Replace content.", "parameters": { "type": "object", "properties": { "file_path": {"type": "string"}, "path": {"type": "string"}, "new_content": {"type": "string"}, }, "required": ["file_path", "path", "new_content"], }, }, }, { "type": "function", "function": { "name": "insert_element", "description": "Insert element.", "parameters": { "type": "object", "properties": { "file_path": {"type": "string"}, "path": {"type": "string"}, "element_type": {"type": "string"}, "content": {"type": "string"}, "where": {"type": "string"}, "heading_level": {"type": "integer"}, }, "required": ["file_path", "path", "element_type", "content"], }, }, }, { "type": "function", "function": { "name": "delete_element", "description": "Delete element.", "parameters": { "type": "object", "properties": { "file_path": {"type": "string"}, "path": {"type": "string"}, }, "required": ["file_path", "path"], }, }, }, { "type": "function", "function": { "name": "move_element", "description": "Move element.", "parameters": { "type": "object", "properties": { "file_path": {"type": "string"}, "source_path": {"type": "string"}, "target_path": {"type": "string"}, "where": {"type": "string"}, }, "required": ["file_path", "source_path", "target_path"], }, }, }, { "type": "function", "function": { "name": "search_text", "description": "Search text.", "parameters": { "type": "object", "properties": { "file_path": {"type": "string"}, "query": {"type": "string"}, }, "required": ["file_path", "query"], }, }, }, { "type": "function", "function": { "name": "get_context", "description": "Get context.", "parameters": { "type": "object", "properties": { "file_path": {"type": "string"}, "path": {"type": "string"}, }, "required": ["file_path", "path"], }, }, }, { "type": "function", "function": { "name": "update_metadata", "description": "Update metadata.", "parameters": { "type": "object", "properties": { "file_path": {"type": "string"}, "metadata": {"type": "object"}, }, "required": ["file_path", "metadata"], }, }, }, { "type": "function", "function": { "name": "undo", "description": "Undo changes.", "parameters": { "type": "object", "properties": { "file_path": {"type": "string"}, "count": {"type": "integer"}, }, "required": ["file_path"], }, }, }, ] async def call_ollama(messages: List[Dict[str, Any]]) -> Dict[str, Any]: """Call Ollama API.""" async with httpx.AsyncClient(timeout=120.0) as client: payload = { "model": MODEL, "messages": messages, "tools": TOOLS_SCHEMA, "stream": False, } try: resp = await client.post(f"{OLLAMA_URL}/api/chat", json=payload) resp.raise_for_status() return resp.json() except Exception as e: print(f"Ollama call failed: {e}") return {{}} async def execute_tool(name: str, args: Dict[str, Any]) -> Any: """Execute tool wrapper.""" print(f" [Tool Exec] {name} args={args}") if name == "list_directory": return await list_directory(args.get("path", ".")) if name == "create_file": return await create_file(args["path"], args.get("content", "")) if name == "delete_item": return await delete_item(args["path"]) if name == "get_document_structure": return await get_document_structure(args["file_path"]) if name == "read_element": return await read_element(args["file_path"], args["path"]) if name == "replace_content": return await replace_content( args["file_path"], args["path"], args["new_content"] ) if name == "insert_element": return await insert_element( args["file_path"], args["path"], args["element_type"], args["content"], args.get("where", "after"), args.get("heading_level", 1), ) if name == "delete_element": return await delete_element(args["file_path"], args["path"]) if name == "move_element": return await move_document_element( args["file_path"], args["source_path"], args["target_path"], args.get("where", "after"), ) if name == "search_text": return await search_in_document(args["file_path"], args["query"]) if name == "get_context": return await get_element_context(args["file_path"], args["path"]) if name == "update_metadata": return await update_document_metadata(args["file_path"], args["metadata"]) if name == "undo": return await undo_changes(args["file_path"], args.get("count", 1)) return {"error": f"Unknown tool: {name}"} @pytest.mark.asyncio async def test_story_workflow_on_testtesat(): """ Runs a full story editing workflow on 'testtesat.md' using Ollama. """ # 1. Setup print("\n=== STARTING OLLAMA STORY WORKFLOW ===\n") # Check Ollama availability try: with httpx.Client(timeout=2.0) as client: resp = client.get(f"{OLLAMA_URL}/api/tags") if resp.status_code != 200: pytest.skip("Ollama not available") except Exception: pytest.skip("Ollama not available") # Use current directory cwd = os.getcwd() PathResolver.set_base_path(cwd) filename = "testtesat.md" if not os.path.exists(filename): pytest.fail(f"{filename} not found!") history = [ { "role": "system", "content": "You are an editor. Use the provided tools to edit the markdown file exactly as requested. Do not output conversational text, just use tools.", } ] async def run_step(prompt): print(f"\n>>> USER: {prompt}") history.append({"role": "user", "content": prompt}) resp = await call_ollama(history) msg = resp.get("message", {}) tool_calls = msg.get("tool_calls", []) if not tool_calls: print(f"!!! NO TOOL CALLS. Response: {msg.get('content')}") return None for tool_call in tool_calls: fname = tool_call["function"]["name"] fargs = tool_call["function"]["arguments"] # Execute result = await execute_tool(fname, fargs) print(f" [Result] {str(result)[:100]}...") # Add to history history.append(msg) # Add assistant's tool call message history.append({"role": "tool", "content": str(result), "name": fname}) return result # Return first result for assertions # --- Workflow Steps --- # 1. List directory to confirm file exists res = await run_step( "Check if 'testtesat.md' exists in current directory using list_directory." ) assert any(f["name"] == filename for f in res) # 2. Get structure res = await run_step(f"Get the structure of '{filename}'.") assert len(res) > 0 # 3. Read specific element # Path depends on structure. Let's assume the known structure from previous turn. # "Хроники Забытого Королевства > Глава 2: Находка > paragraph 2" target_path = "Хроники Забытого Королевства > Глава 2: Находка > paragraph 2" res = await run_step(f"Read the content of '{target_path}' in '{filename}'.") assert "ржавый" in res.get("content", "") # 4. Search text res = await run_step(f"Search for 'Артур' in '{filename}'.") assert len(res) >= 2 # 5. Get context res = await run_step(f"Get context for '{target_path}' in '{filename}'.") assert "current" in res # 6. Replace content new_text = "Он сиял магическим светом." res = await run_step( f"Change content of '{target_path}' in '{filename}' to '{new_text}'." ) assert res.get("success") is True # 7. Insert new chapter res = await run_step( f"Add a new heading 'Глава 3: Путь' after 'Хроники Забытого Королевства > Глава 2: Находка' in '{filename}'." ) assert res.get("success") is True # 8. Update metadata res = await run_step(f"Set author to 'Merlin' in metadata for '{filename}'.") assert res.get("success") is True # 9. Create a copy (File Ops) res = await run_step( "Create a file 'copy_test.md' with content 'Backup' using create_file." ) assert res.get("success") is True # 10. Delete the copy res = await run_step("Delete the file 'copy_test.md'.") assert res.get("success") is True # 11. Undo last change (metadata update or delete? Undo works on document edit, not file ops usually) # The last DOCUMENT edit was Update Metadata. res = await run_step(f"Undo the last change to '{filename}'.") assert res.get("success") is True print("\n=== WORKFLOW COMPLETED SUCCESSFULLY ===") if __name__ == "__main__": pytest.main([__file__, "-v", "-s"])

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/KazKozDev/markdown-editor-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server