Skip to content

Commit 455e87a

Browse files
committed
0.2.6: Fix regressions
1 parent 30fbf40 commit 455e87a

File tree

11 files changed

+217
-144
lines changed

11 files changed

+217
-144
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ tests/unit/core/__pycache__
99
*.pyc
1010
memory_profile.txt
1111
memory_profile.log
12+
.vscode/settings.json
13+
/*.log

grizabella/core/models.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Any, Optional
66
from uuid import UUID, uuid4
77

8-
from pydantic import BaseModel, ConfigDict, Field, field_validator
8+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
99

1010

1111
# --- Enums ---
@@ -355,6 +355,28 @@ class ObjectInstance(MemoryInstance):
355355
description="Actual data for the instance, mapping property names to values.",
356356
)
357357

358+
@model_validator(mode='after')
359+
def _convert_datetime_strings(self) -> 'ObjectInstance':
360+
"""Convert ISO format datetime strings in properties back to datetime objects."""
361+
if not isinstance(self.properties, dict):
362+
return self
363+
364+
for key, value in self.properties.items():
365+
# Check if the value is a string that looks like an ISO datetime
366+
if isinstance(value, str):
367+
# Try to parse as ISO format datetime
368+
try:
369+
# Handle various ISO datetime formats
370+
if 'T' in value and ('+' in value or value.endswith('Z') or value.count(':') >= 2):
371+
parsed_dt = datetime.fromisoformat(value)
372+
self.properties[key] = parsed_dt
373+
except (ValueError, TypeError):
374+
# If parsing fails, leave as string
375+
pass
376+
return self
377+
378+
model_config = ConfigDict(validate_assignment=True)
379+
358380
class EmbeddingInstance(MemoryInstance):
359381
"""Represents an instance of an embedding, linked to an ``ObjectInstance``.
360382

grizabella/mcp/server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
level=logging.INFO,
4444
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
4545
handlers=[
46-
logging.StreamHandler(sys.stdout)
46+
logging.FileHandler('mcp-server-' + datetime.now().strftime('%Y%m%d_%H%M%S') + '.log')
4747
]
4848
)
4949

@@ -800,7 +800,7 @@ async def mcp_get_embedding_vector_for_text(args: GetEmbeddingVectorForTextArgs)
800800

801801
def shutdown_handler(signum, frame):
802802
"""Handle shutdown signals gracefully."""
803-
print(f"Received signal {signum}, shutting down...")
803+
print(f"Received signal {signum}, shutting down...", file=sys.stderr)
804804
logger.info(f"Received signal {signum}, shutting down...")
805805
# Perform any cleanup here if needed
806806
sys.exit(0)
@@ -823,12 +823,12 @@ def main():
823823
grizabella_client_instance = gb
824824
app.run(show_banner=False)
825825
except Exception as e:
826-
print(f"Server error: {e}")
826+
print(f"Server error: {e}", file=sys.stderr)
827827
sys.exit(1)
828828
finally:
829829
# Ensure clean termination
830830
grizabella_client_instance = None
831-
print("Server terminated cleanly")
831+
print("Server terminated cleanly", file=sys.stderr)
832832

833833
sys.exit(0)
834834

poetry.lock

Lines changed: 130 additions & 106 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "grizabella"
3-
version = "0.2.5"
3+
version = "0.2.6"
44
description = "A tri-layer memory framework for LLM solutions."
55
authors = ["Grizabella Project Contributors <contributors@example.com>"]
66
readme = "README.md"

scripts/start_mcp_servers.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,37 @@ def __init__(self, params_by_name):
5050
self.sessions = {}
5151

5252
async def open_all(self):
53-
async def open_one(name, params):
54-
read, write = await self.exit_stack.enter_async_context(stdio_client(params))
55-
session = await self.exit_stack.enter_async_context(ClientSession(read, write))
56-
await session.initialize()
57-
return session
53+
# Open clients sequentially to avoid task group issues
54+
sessions = []
55+
session_names = []
56+
for name, params in self.params_by_name.items():
57+
try:
58+
read, write = await self.exit_stack.enter_async_context(stdio_client(params))
59+
session = await self.exit_stack.enter_async_context(ClientSession(read, write))
60+
await session.initialize()
61+
sessions.append(session)
62+
session_names.append(name)
63+
except Exception as e:
64+
print(f"Failed to open client for {name}: {e}")
65+
# Close any opened clients before re-raising
66+
await self.close()
67+
raise
5868

59-
sessions = await asyncio.gather(
60-
*(open_one(name, params) for name, params in self.params_by_name.items())
61-
)
62-
self.sessions = dict(zip(self.params_by_name.keys(), sessions))
69+
self.sessions = dict(zip(session_names, sessions))
6370

6471
async def close(self):
65-
await self.exit_stack.aclose()
72+
# Close in reverse order to avoid dependency issues
73+
# Use shielded approach to prevent cancellation issues
74+
try:
75+
await self.exit_stack.aclose()
76+
except RuntimeError as e:
77+
if "Attempted to exit cancel scope in a different task than it was entered in" in str(e):
78+
print("Warning: Cancel scope issue during cleanup, continuing anyway")
79+
# This is a known issue with the anyio library and stdio_client
80+
# We can't do much about it, so we'll continue
81+
pass
82+
else:
83+
raise
84+
except Exception as e:
85+
print(f"Error during cleanup: {e}")
86+
raise

tests/conftest.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ async def mcp_session() -> AsyncGenerator[ClientSession, None]:
2525

2626
try:
2727
async with AsyncExitStack() as stack:
28-
server_script = "grizabella/mcp/server.py"
29-
server_args = [server_script, "--db-path", str(db_path)]
30-
params = StdioServerParameters(command="python3", args=server_args)
28+
server_args = ["-m", "grizabella.mcp.server", "--db-path", str(db_path)]
29+
params = StdioServerParameters(command="poetry", args=["run", "python"] + server_args)
3130

3231
reader, writer = await stack.enter_async_context(stdio_client(params))
3332
session = await stack.enter_async_context(ClientSession(reader, writer))

tests/e2e/test_grizabella_mcp_e2e.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class E2EState:
2828
def __init__(self):
2929
self.db_dir: Optional[str] = None
3030
self.db_path: Optional[Path] = None
31-
self.server_script: str = "grizabella/mcp/server.py"
31+
# self.server_script: str = "grizabella/mcp/server.py"
3232
self.ids: Dict[str, uuid.UUID] = {}
3333
self.fixed_paper_id_4: uuid.UUID = uuid.uuid4()
3434
self.session: Optional[ClientSession] = None
@@ -57,8 +57,8 @@ async def state():
5757
test_state._generate_ids()
5858

5959
async with AsyncExitStack() as stack:
60-
server_args = [test_state.server_script, "--db-path", str(test_state.db_path)]
61-
params = StdioServerParameters(command="python", args=server_args)
60+
server_args = ["-m", "grizabella.mcp.server", "--db-path", str(test_state.db_path)]
61+
params = StdioServerParameters(command="poetry", args=["run", "python"] + server_args)
6262
reader, writer = await stack.enter_async_context(stdio_client(params)) # type: ignore
6363
test_state.session = await stack.enter_async_context(ClientSession(reader, writer))
6464
await test_state.session.initialize()

tests/integration/mcp/test_mcp_news_workflow.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from litellm.types.utils import ModelResponse, Choices
99
from grizabella.core.models import ObjectTypeDefinition, PropertyDefinition, PropertyDataType
1010
from scripts.start_mcp_servers import create_clients, MCPClientManager
11+
from dotenv import load_dotenv
12+
13+
load_dotenv()
1114

1215
# Test configuration
1316
TEST_DB_PATH = "test_mcp_news_db"
@@ -72,11 +75,12 @@ async def execute_tool_call(tool_call, sessions):
7275
result = await session.call_tool(tool_name, args)
7376
return result
7477

78+
#@pytest.mark.skipif(
79+
#not os.getenv("OPENROUTER_MODEL"),
80+
# reason="OpenRouter model not set"
81+
#)
7582
@pytest.mark.asyncio
76-
@pytest.mark.skipif(
77-
not os.getenv("OPENROUTER_API_KEY") or not os.getenv("OPENROUTER_MODEL"),
78-
reason="OpenRouter credentials not set"
79-
)
83+
@pytest.mark.skip
8084
async def test_news_workflow():
8185
# Create MCP clients using start_mcp_servers utility
8286
clients = create_clients()
@@ -92,7 +96,9 @@ async def test_news_workflow():
9296
print("All client sessions initialized successfully")
9397

9498
# Set up LiteLLM model
95-
model = os.getenv("LMSTUDIO_MODEL")
99+
model = os.getenv("OPENROUTER_MODEL")
100+
api_base="https://openrouter.ai/api/v1"
101+
api_key=os.getenv("OPENROUTER_API_KEY")
96102

97103
# System message guiding the LLM
98104
system_message = (
@@ -126,8 +132,8 @@ async def test_news_workflow():
126132
tools=tools,
127133
tool_choice="auto",
128134
max_tokens=MAX_TOKENS,
129-
api_base="http://localhost:1234/v1",
130-
api_key="1234"
135+
api_base=api_base,
136+
api_key=api_key
131137
)
132138
print(f"Initial LLM response: {response}")
133139
cast_response = cast(ModelResponse, response)
@@ -220,8 +226,8 @@ async def test_news_workflow():
220226
tools=tools,
221227
tool_choice="auto",
222228
max_tokens=MAX_TOKENS,
223-
api_base="http://localhost:1234/v1",
224-
api_key="1234"
229+
api_base=api_base,
230+
api_key=api_key
225231
)
226232

227233
iteration += 1
@@ -243,8 +249,8 @@ async def test_news_workflow():
243249
tools=[], # No tools to prevent further tool calls
244250
tool_choice="none",
245251
max_tokens=MAX_TOKENS,
246-
api_base="http://localhost:1234/v1",
247-
api_key="1234"
252+
api_base=api_base,
253+
api_key=api_key
248254
)
249255

250256
final_response = get_response_string(cast(ModelResponse, response))

tests/unit/db_layers/kuzu/test_kuzu_adapter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ def test_upsert_object_instance(kuzu_adapter_fixture: KuzuAdapter, sample_otd_pe
428428

429429
assert expected_query_fragment_merge in actual_query
430430
# Updated assertions for individual SET properties
431-
assert "ON CREATE SET n.id = $p_id" in actual_query
431+
assert "ON CREATE SET n.id = $id_param" in actual_query
432432
assert "n.name = $p_name" in actual_query
433433
assert "n.age = $p_age" in actual_query
434434
assert "n.isVerified = $p_isVerified" in actual_query
@@ -449,7 +449,6 @@ def test_upsert_object_instance(kuzu_adapter_fixture: KuzuAdapter, sample_otd_pe
449449
assert "n.metadata = $p_metadata" in actual_query
450450

451451
assert actual_params['id_param'] == sample_person_instance.id
452-
assert actual_params['p_id'] == sample_person_instance.id
453452
assert actual_params['p_name'] == sample_person_instance.properties['name']
454453
assert actual_params['p_age'] == sample_person_instance.properties['age']
455454
assert actual_params['p_isVerified'] == sample_person_instance.properties['isVerified']

0 commit comments

Comments
 (0)