Skip to content

Commit 073ceba

Browse files
committed
Fix asyncio.run() error in novelty checking
Update novelty LLM check to handle calls from both async and sync contexts, using ThreadPoolExecutor when an event loop is running. Add tests to verify correct behavior in both contexts and address issue #313.
1 parent cdfb022 commit 073ceba

File tree

2 files changed

+196
-11
lines changed

2 files changed

+196
-11
lines changed

openevolve/database.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -975,20 +975,36 @@ def _llm_judge_novelty(self, program: Program, similar_program: Program) -> bool
975975
"""
976976
import asyncio
977977
from openevolve.novelty_judge import NOVELTY_SYSTEM_MSG, NOVELTY_USER_MSG
978-
978+
979979
user_msg = NOVELTY_USER_MSG.format(
980980
language=program.language,
981981
existing_code=similar_program.code,
982982
proposed_code=program.code,
983983
)
984-
984+
985985
try:
986-
content: str = asyncio.run(
987-
self.novelty_llm.generate_with_context(
988-
system_message=NOVELTY_SYSTEM_MSG,
989-
messages=[{"role": "user", "content": user_msg}],
986+
# Check if we're already in an event loop
987+
try:
988+
loop = asyncio.get_running_loop()
989+
# We're in an async context, need to run in a new thread
990+
import concurrent.futures
991+
with concurrent.futures.ThreadPoolExecutor() as executor:
992+
future = executor.submit(
993+
asyncio.run,
994+
self.novelty_llm.generate_with_context(
995+
system_message=NOVELTY_SYSTEM_MSG,
996+
messages=[{"role": "user", "content": user_msg}],
997+
)
998+
)
999+
content: str = future.result()
1000+
except RuntimeError:
1001+
# No event loop running, safe to use asyncio.run()
1002+
content: str = asyncio.run(
1003+
self.novelty_llm.generate_with_context(
1004+
system_message=NOVELTY_SYSTEM_MSG,
1005+
messages=[{"role": "user", "content": user_msg}],
1006+
)
9901007
)
991-
)
9921008

9931009
if content is None or content is None:
9941010
logger.warning("Novelty LLM returned empty response")
@@ -999,24 +1015,24 @@ def _llm_judge_novelty(self, program: Program, similar_program: Program) -> bool
9991015
# Parse the response
10001016
NOVEL_i = content.upper().find("NOVEL")
10011017
NOT_NOVEL_i = content.upper().find("NOT NOVEL")
1002-
1018+
10031019
if NOVEL_i == -1 and NOT_NOVEL_i == -1:
10041020
logger.warning(f"Unexpected novelty LLM response: {content}")
10051021
return True # Assume novel if we can't parse
1006-
1022+
10071023
if NOVEL_i != -1 and NOT_NOVEL_i != -1:
10081024
# Both found, take the one that appears first
10091025
is_novel = NOVEL_i < NOT_NOVEL_i
10101026
elif NOVEL_i != -1:
10111027
is_novel = True
10121028
else:
10131029
is_novel = False
1014-
1030+
10151031
return is_novel
10161032

10171033
except Exception as e:
10181034
logger.error(f"Error in novelty LLM check: {e}")
1019-
1035+
10201036
return True
10211037

10221038
def _is_novel(self, program_id: int, island_idx: int) -> bool:
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""
2+
Test for issue #313: asyncio.run() error in novelty checking
3+
https://github.com/algorithmicsuperintelligence/openevolve/issues/313
4+
5+
This test reproduces the bug where calling database.add() from within an async context
6+
triggers a novelty check that uses asyncio.run(), which fails because it's already
7+
running in an event loop.
8+
"""
9+
10+
import unittest
11+
import asyncio
12+
from unittest.mock import AsyncMock, MagicMock, patch, Mock
13+
from openevolve.config import Config
14+
from openevolve.database import Program, ProgramDatabase
15+
16+
17+
class MockLLM:
18+
"""Mock LLM that implements the async interface"""
19+
20+
async def generate_with_context(self, system_message: str, messages: list):
21+
"""Mock async generate method that returns NOVEL"""
22+
return "NOVEL"
23+
24+
25+
class TestNoveltyAsyncioIssue(unittest.TestCase):
26+
"""Test for asyncio.run() error in novelty checking (issue #313)"""
27+
28+
@patch('openevolve.embedding.EmbeddingClient')
29+
def setUp(self, mock_embedding_client_class):
30+
"""Set up test database with novelty checking enabled"""
31+
# Mock the embedding client
32+
mock_instance = MagicMock()
33+
mock_instance.get_embedding.return_value = [0.1] * 1536 # Mock embedding vector
34+
mock_embedding_client_class.return_value = mock_instance
35+
36+
config = Config()
37+
config.database.in_memory = True
38+
config.database.embedding_model = "text-embedding-3-small"
39+
config.database.similarity_threshold = 0.99
40+
config.database.novelty_llm = MockLLM()
41+
42+
self.db = ProgramDatabase(config.database)
43+
self.mock_embedding_client_class = mock_embedding_client_class
44+
45+
def test_novelty_check_from_async_context_works(self):
46+
"""
47+
Test that novelty checking works correctly when called from within
48+
an async context (this was the bug in issue #313).
49+
50+
Expected behavior: Should successfully run the novelty check without
51+
any asyncio.run() errors, properly using ThreadPoolExecutor to handle
52+
the async LLM call from within a running event loop.
53+
"""
54+
import logging
55+
56+
# Create two programs with similar embeddings to trigger LLM novelty check
57+
program1 = Program(
58+
id="prog1",
59+
code="def test(): return 1",
60+
language="python",
61+
metrics={"score": 0.5},
62+
)
63+
64+
program2 = Program(
65+
id="prog2",
66+
code="def test(): return 2",
67+
language="python",
68+
metrics={"score": 0.6},
69+
parent_id="prog1",
70+
)
71+
72+
async def async_add_programs():
73+
"""Add programs from async context - this simulates controller.run()"""
74+
# Add first program (no novelty check, no similar programs yet)
75+
prog1_id = self.db.add(program1)
76+
self.assertIsNotNone(prog1_id)
77+
78+
# Add second program - this triggers novelty check
79+
# Since embeddings are similar (both [0.1] * 1536), it will call
80+
# _llm_judge_novelty which should now work correctly
81+
prog2_id = self.db.add(program2)
82+
83+
# The novelty check should succeed without errors
84+
# The program should be added (MockLLM returns "NOVEL")
85+
self.assertIsNotNone(prog2_id)
86+
87+
return True
88+
89+
# This should work without any errors now
90+
result = asyncio.run(async_add_programs())
91+
self.assertTrue(result)
92+
93+
# Verify both programs were added
94+
self.assertIn("prog1", self.db.programs)
95+
self.assertIn("prog2", self.db.programs)
96+
97+
def test_novelty_check_from_sync_context_works(self):
98+
"""
99+
Test that novelty checking also works correctly when called from
100+
a synchronous (non-async) context.
101+
102+
Expected behavior: Should successfully run the novelty check using
103+
asyncio.run() since there's no running event loop.
104+
"""
105+
# Create two programs with similar embeddings to trigger LLM novelty check
106+
program1 = Program(
107+
id="prog3",
108+
code="def test(): return 3",
109+
language="python",
110+
metrics={"score": 0.5},
111+
)
112+
113+
program2 = Program(
114+
id="prog4",
115+
code="def test(): return 4",
116+
language="python",
117+
metrics={"score": 0.6},
118+
parent_id="prog3",
119+
)
120+
121+
# Add programs from synchronous context (no event loop running)
122+
prog1_id = self.db.add(program1)
123+
self.assertIsNotNone(prog1_id)
124+
125+
prog2_id = self.db.add(program2)
126+
self.assertIsNotNone(prog2_id)
127+
128+
# Verify both programs were added
129+
self.assertIn("prog3", self.db.programs)
130+
self.assertIn("prog4", self.db.programs)
131+
132+
def test_novelty_check_disabled_works_fine(self):
133+
"""
134+
Test that when novelty checking is disabled, adding programs
135+
from async context works fine (this is the workaround from issue #313).
136+
"""
137+
# Create a new database with novelty checking disabled
138+
config = Config()
139+
config.database.in_memory = True
140+
config.database.similarity_threshold = 0.0 # Disable novelty checking
141+
db_no_novelty = ProgramDatabase(config.database)
142+
143+
program1 = Program(
144+
id="prog1",
145+
code="def test(): return 1",
146+
language="python",
147+
metrics={"score": 0.5},
148+
)
149+
150+
program2 = Program(
151+
id="prog2",
152+
code="def test(): return 2",
153+
language="python",
154+
metrics={"score": 0.6},
155+
)
156+
157+
async def async_add_programs():
158+
"""Add programs from async context"""
159+
db_no_novelty.add(program1)
160+
db_no_novelty.add(program2)
161+
return True
162+
163+
# This should work fine without novelty checking
164+
result = asyncio.run(async_add_programs())
165+
self.assertTrue(result)
166+
167+
168+
if __name__ == "__main__":
169+
unittest.main()

0 commit comments

Comments
 (0)