-
Notifications
You must be signed in to change notification settings - Fork 731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Source2synth #1289
base: master
Are you sure you want to change the base?
feat: Source2synth #1289
Changes from 66 commits
0e31d55
ea73cbb
a8be4a4
3141161
87c6bb8
3b627e0
205af73
62e5728
10f17aa
e5d961f
760cafd
36e3005
4d7e244
586babb
c9812d1
2473ccd
f96d726
5f86331
640ba82
40014fe
a7d87a2
2330c6e
9b21bae
3816c24
9d7d76a
6deb08a
3c90f59
3548528
d15cccb
9e8fe98
7adcc74
ba5d5a9
62602d7
a0c9cae
727982b
16eb58e
02b87e8
bf90830
8800962
19f1ab3
95cd598
e7131f4
c0bc6b5
da461a8
2ae2cdc
e527a9b
301e803
8be1a92
510c63e
aa20f1b
98a40a3
b020c91
dd7d14b
a77b778
2a82479
7e2499c
aae9d8f
836e798
21362cc
90735d6
0f4a96b
4b38f50
0f423f6
0e01d86
9871f52
dfc0f9f
2667f2c
41aaf8e
ccb5a32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= | ||
|
||
from typing import Any | ||
|
||
from pydantic import ConfigDict | ||
|
||
from camel.agents.programmed_agent_instruction import ( | ||
ProgrammableChatAgent, | ||
ProgrammedAgentInstructionResult, | ||
programmable_capability, | ||
) | ||
from camel.messages import BaseMessage | ||
from camel.synthetic_datagen.source2synth.models import ( | ||
ContextPrompt, | ||
MultiHopQA, | ||
) | ||
|
||
|
||
class MultiHopGeneratorAgent(ProgrammableChatAgent): | ||
model_config = ConfigDict(arbitrary_types_allowed=True) | ||
|
||
def __init__(self, **kwargs: Any): | ||
super().__init__(**kwargs) | ||
|
||
system_text: str = """You are an expert at generating multi-hop | ||
question-answer pairs. | ||
For each context, you should: | ||
1. Identify multiple related facts or pieces of information | ||
2. Create questions that require reasoning across these multiple pieces | ||
3. Ensure the reasoning chain is clear and logical | ||
4. Generate questions that require at least 2-3 steps of reasoning | ||
5. Include the reasoning steps in the answer | ||
|
||
Give your response with this information: | ||
Question: [Complex question requiring multiple reasoning steps] | ||
Reasoning Steps: | ||
1. [First reasoning step] | ||
2. [Second reasoning step] | ||
3. [Final reasoning step] | ||
Answer: [Final answer] | ||
Supporting Facts: [List of relevant text segments used] | ||
""" | ||
self.system_message = BaseMessage.make_assistant_message( | ||
role_name='Assistant', content=system_text | ||
) | ||
|
||
@programmable_capability | ||
def generate_multi_hop_qa( | ||
self, context: str | ||
) -> ProgrammedAgentInstructionResult[MultiHopQA]: | ||
context_prompt = ContextPrompt( | ||
main_context=context, related_contexts=None | ||
) | ||
|
||
user_message = BaseMessage.make_user_message( | ||
content=context_prompt.model_dump_json(), role_name="User" | ||
) | ||
response = self.step( | ||
input_message=user_message, response_format=MultiHopQA | ||
) | ||
value = MultiHopQA.model_validate_json(response.msgs[0].content) | ||
|
||
if response.msgs: | ||
return ProgrammedAgentInstructionResult( | ||
user_message=user_message, | ||
agent_message=response.msgs[0], | ||
value=value, | ||
) | ||
raise RuntimeError("No response from agent") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= | ||
import abc | ||
import threading | ||
from enum import Enum | ||
from functools import wraps | ||
from typing import Any, Callable, Generic, Optional, TypeVar | ||
|
||
from pydantic import BaseModel, ConfigDict | ||
|
||
from camel.agents import ChatAgent | ||
from camel.messages import BaseMessage | ||
|
||
T = TypeVar('T') | ||
|
||
|
||
class ProgrammableAgentRequirement(Enum): | ||
LAST_MESSAGE_NOT_USER = "LAST_MESSAGE_NOT_USER" | ||
Comment on lines
+28
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think it's not necessary to define this enum class just for 1 compont, maybe we can pass the variable directly to reduce code complexibility There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My thinking for this is that with the requirements being an enum, implementors can easily know for example that all possible requirements are being met, since requirements are likely to be shared between different agents (as can the operations that modify agent state with them in consideration). The intention here is to sort of force certainty improving code to be separate from the functional code, which can be really beneficial for keeping things expectable in the future. Though maybe the possible requirements should be part of a type parameter on the ProgrammableChatAgent implementation (several subclasses with different numbers of type parameters might be necessary until pydantic supports variadic generics), since they should be relevant only to the I wanted to allow specifying the agent state requirements in the There might be a nice way of doing that still though, like through providing a wrapping interface to the agent methods that requires explicitly stating the state expectations, and possibly having these return a sort of atomic operation accumulating object like what torch does. Incompatible operation chains then can be easily detected before execution, and the business logic can more easily coerce state into being compatible before starting the operation chain. I think the direction here is pretty important for programmed composable agents, paging @lightaime for thoughts. For now though, I think this direction is okay, it means requirements need to be shared between functions inside an agent, and can be easily enough changed to a different method in the future, we should maybe just encourage people to put the state requirements in this enum |
||
|
||
|
||
class ProgrammedAgentInstructionResult(BaseModel, Generic[T]): | ||
r""" | ||
Result of a programmable agent instruction execution. | ||
CaelumF marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Contains the messages exchanged during execution and the computed value. | ||
The value type is specified by the generic type parameter T. | ||
""" | ||
|
||
user_message: BaseMessage | ||
agent_message: BaseMessage | ||
value: T | ||
|
||
model_config = ConfigDict(arbitrary_types_allowed=True) | ||
|
||
|
||
class AbstractProgrammableAgent(abc.ABC): | ||
r""" | ||
Abstract class for a programmable agent. | ||
CaelumF marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
A programmable agent is an agent that can be programmed to perform a | ||
specific function or task. This class defines the interface for a | ||
programmable | ||
agent. | ||
|
||
These methods should be implemented in order to ensure the agent supports | ||
the necessary guarantees to enable a programming interface while | ||
maintaining compatibility in a multi-agent system. | ||
|
||
A programmable agent is responsible for providing and maintaining a | ||
programming interface for its functionality. | ||
""" | ||
|
||
@abc.abstractmethod | ||
def run_atomic( | ||
self, callback: Callable[[], ProgrammedAgentInstructionResult[T]] | ||
) -> ProgrammedAgentInstructionResult[T]: | ||
r""" | ||
Run an atomic operation on the agent. | ||
CaelumF marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
An atomic operation is an operation that is guaranteed to | ||
be executed without interruption by any other operation. | ||
|
||
If the operation fails or times out the agents state should be | ||
unchanged. | ||
|
||
If an operation is already in progress, this method should throw an | ||
exception. (It is up to the caller to do any queuing) | ||
|
||
If the agent is in a state where it can perform the operation, | ||
it must leave the agent in a state where it can perform the | ||
operation again. Though if state changes in successful operation | ||
improve its ability to perform the operation, it should keep them. | ||
""" | ||
raise NotImplementedError | ||
|
||
@abc.abstractmethod | ||
def repair_state(self, requirement: ProgrammableAgentRequirement) -> None: | ||
r""" | ||
Repair the state of the agent. | ||
|
||
Agents may have other non-atomic interfaces, such as a user interface, | ||
or chat between other agents. | ||
|
||
This method should restore the agent to a state where it can perform | ||
operations according to the specified requirement. | ||
""" | ||
raise NotImplementedError | ||
|
||
|
||
def programmable_capability( | ||
func: Callable[..., ProgrammedAgentInstructionResult[T]], | ||
) -> Callable[..., ProgrammedAgentInstructionResult[T]]: | ||
r""" | ||
Decorator for programmable agent capabilities. | ||
|
||
Wraps a method to ensure it is executed atomically via the agent's | ||
run_atomic interface. | ||
The decorated method must return a ProgrammedAgentInstructionResult with | ||
appropriate type parameter. | ||
""" | ||
|
||
@wraps(func) | ||
def wrapper( | ||
self, *args: Any, **kwargs: Any | ||
) -> ProgrammedAgentInstructionResult[T]: | ||
return self.run_atomic(lambda: func(self, *args, **kwargs)) | ||
|
||
return wrapper | ||
|
||
|
||
class ProgrammableChatAgent(ChatAgent, AbstractProgrammableAgent): | ||
r""" | ||
A chat agent that can be programmed to perform specific tasks. | ||
|
||
Provides a default implementation of atomic execution using threading locks | ||
and basic state tracking for message roles. Implementing classes need to | ||
provide specific repair logic for their use cases. | ||
""" | ||
|
||
def __init__(self, **kwargs: Any): | ||
super().__init__(**kwargs) | ||
self._operation_lock = threading.Lock() | ||
self._last_message_role: Optional[str] = None | ||
|
||
def run_atomic( | ||
self, callback: Callable[[], ProgrammedAgentInstructionResult[T]] | ||
) -> ProgrammedAgentInstructionResult[T]: | ||
if not self._operation_lock.acquire(blocking=False): | ||
raise RuntimeError("Operation already in progress") | ||
|
||
try: | ||
result = callback() | ||
self._last_message_role = result.agent_message.role_name | ||
return result | ||
finally: | ||
self._operation_lock.release() | ||
|
||
def repair_state(self, requirement: ProgrammableAgentRequirement) -> None: | ||
if requirement == ProgrammableAgentRequirement.LAST_MESSAGE_NOT_USER: | ||
if self._last_message_role == "user": | ||
raise NotImplementedError( | ||
"Must implement repair for LAST_MESSAGE_NOT_USER" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can use
textwrap.dedent()
to eliminate the leading spaces.