-
Notifications
You must be signed in to change notification settings - Fork 274
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
agentops_property - internal objects tracking the right way (#506)
* AgentProperty descriptor The function `check_call_stack_for_agent_id` is designed to search through the call stack to find an instance of a class that contains specific `AgentProperty` descriptors, particularly looking for an `agent_ops_agent_id`. Here's a step-by-step explanation:1. **Function Definition and Docstring**: - The function is defined to return either a `UUID` or `None`. - The docstring explains that it looks through the call stack for the class that called the LLM (Language Learning Model) and checks for `AgentProperty` descriptors.2. **Inspecting the Call Stack**: - The function uses `inspect.stack()` to get the current call stack, which is a list of `FrameInfo` objects representing the stack frames.3. **Iterating Through Stack Frames**: - It iterates over each `frame_info` in the call stack.4. **Accessing Local Variables**: - For each frame, it accesses the local variables using `frame_info.frame.f_locals`.5. **Checking Each Local Variable**: - It iterates over each local variable (`var_name`, `var`) in the current frame.6. **Stopping at Main**: - If the variable name is `"__main__"`, it returns `None` and stops further processing.7. **Checking for AgentProperty Descriptors**: - It tries to check if the variable (`var`) has `AgentProperty` descriptors. - It gets the type of the variable (`var_type`). - It retrieves all class attributes of `var_type` into a dictionary `class_attrs`.8. **Looking for Specific Descriptors**: - It looks for an attribute named `agent_ops_agent_id` in `class_attrs`. - If `agent_ops_agent_id` is an instance of `AgentProperty`, it retrieves the agent ID using `agent_id_desc.__get__(var, var_type)`.9. **Returning the Agent ID**: - If an agent ID is found, it optionally retrieves the agent name using a similar process and returns the agent ID. - If no agent ID is found, it continues to the next variable.10. **Handling Exceptions**: - If any exception occurs during the process, it catches the exception and continues to the next variable.11. **Returning None**: - If no agent ID is found in the entire call stack, it returns `None`.This function is useful for debugging or tracking purposes, where you need to identify the agent that initiated a particular call in a complex system. Signed-off-by: Teo <teocns@gmail.com> * fmt Signed-off-by: Teo <teocns@gmail.com> * agentops_property, __set_name__ to automatically handle attribute naming 1. Renamed AgentOpsDescriptor to agentops_property; 2. Thanks to __set_name__, The descriptor will now automatically know its own name when it's assigned as a class attribute Signed-off-by: Teo <teocns@gmail.com> * test: update agent property tests, add more coverage on call stacks * black Signed-off-by: Teo <teocns@gmail.com> * refactor(decorators): using public name accessor * Big W * ruff Signed-off-by: Teo <teocns@gmail.com> --------- Signed-off-by: Teo <teocns@gmail.com>
- Loading branch information
Showing
4 changed files
with
457 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import inspect | ||
import logging | ||
from typing import Union | ||
from uuid import UUID | ||
|
||
|
||
class agentops_property: | ||
""" | ||
A descriptor that provides a standardized way to handle agent property access and storage. | ||
Properties are automatically stored with an '_agentops_' prefix to avoid naming conflicts. | ||
The descriptor can be used in two ways: | ||
1. As a class attribute directly | ||
2. Added dynamically through a decorator (like @track_agent) | ||
Attributes: | ||
private_name (str): The internal name used for storing the property value, | ||
prefixed with '_agentops_'. Set either through __init__ or __set_name__. | ||
Example: | ||
```python | ||
# Direct usage in a class | ||
class Agent: | ||
name = agentops_property() | ||
id = agentops_property() | ||
def __init__(self): | ||
self.name = "Agent1" # Stored as '_agentops_name' | ||
self.id = "123" # Stored as '_agentops_id' | ||
# Usage with decorator | ||
@track_agent() | ||
class Agent: | ||
pass | ||
# agentops_agent_id and agentops_agent_name are added automatically | ||
``` | ||
Notes: | ||
- Property names with 'agentops_' prefix are automatically stripped when creating | ||
the internal storage name | ||
- Returns None if the property hasn't been set | ||
- The descriptor will attempt to resolve property names even when added dynamically | ||
""" | ||
|
||
def __init__(self, name=None): | ||
""" | ||
Initialize the descriptor. | ||
Args: | ||
name (str, optional): The name for the property. Used as fallback when | ||
the descriptor is added dynamically and __set_name__ isn't called. | ||
""" | ||
self.private_name = None | ||
if name: | ||
self.private_name = f"_agentops_{name.replace('agentops_', '')}" | ||
|
||
def __set_name__(self, owner, name): | ||
""" | ||
Called by Python when the descriptor is defined directly in a class. | ||
Sets up the private name used for attribute storage. | ||
Args: | ||
owner: The class that owns this descriptor | ||
name: The name given to this descriptor in the class | ||
""" | ||
self.private_name = f"_agentops_{name.replace('agentops_', '')}" | ||
|
||
def __get__(self, obj, objtype=None): | ||
""" | ||
Get the property value. | ||
Args: | ||
obj: The instance to get the property from | ||
objtype: The class of the instance | ||
Returns: | ||
The property value, or None if not set | ||
The descriptor itself if accessed on the class rather than an instance | ||
Raises: | ||
AttributeError: If the property name cannot be determined | ||
""" | ||
if obj is None: | ||
return self | ||
|
||
# Handle case where private_name wasn't set by __set_name__ | ||
if self.private_name is None: | ||
# Try to find the name by looking through the class dict | ||
for name, value in type(obj).__dict__.items(): | ||
if value is self: | ||
self.private_name = f"_agentops_{name.replace('agentops_', '')}" | ||
break | ||
if self.private_name is None: | ||
raise AttributeError("Property name could not be determined") | ||
|
||
# First try getting from object's __dict__ (for Pydantic) | ||
if hasattr(obj, "__dict__"): | ||
dict_value = obj.__dict__.get(self.private_name[1:]) | ||
if dict_value is not None: | ||
return dict_value | ||
|
||
# Fall back to our private storage | ||
return getattr(obj, self.private_name, None) | ||
|
||
def __set__(self, obj, value): | ||
""" | ||
Set the property value. | ||
Args: | ||
obj: The instance to set the property on | ||
value: The value to set | ||
Raises: | ||
AttributeError: If the property name cannot be determined | ||
""" | ||
if self.private_name is None: | ||
# Same name resolution as in __get__ | ||
for name, val in type(obj).__dict__.items(): | ||
if val is self: | ||
self.private_name = f"_agentops_{name.replace('agentops_', '')}" | ||
break | ||
if self.private_name is None: | ||
raise AttributeError("Property name could not be determined") | ||
|
||
# Set in both object's __dict__ (for Pydantic) and our private storage | ||
if hasattr(obj, "__dict__"): | ||
obj.__dict__[self.private_name[1:]] = value | ||
setattr(obj, self.private_name, value) | ||
|
||
def __delete__(self, obj): | ||
""" | ||
Delete the property value. | ||
Args: | ||
obj: The instance to delete the property from | ||
Raises: | ||
AttributeError: If the property name cannot be determined | ||
""" | ||
if self.private_name is None: | ||
raise AttributeError("Property name could not be determined") | ||
try: | ||
delattr(obj, self.private_name) | ||
except AttributeError: | ||
pass | ||
|
||
@staticmethod | ||
def stack_lookup() -> Union[UUID, None]: | ||
""" | ||
Look through the call stack to find an agent ID. | ||
This method searches the call stack for objects that have agentops_property | ||
descriptors and returns the agent_id if found. | ||
Returns: | ||
UUID: The agent ID if found in the call stack | ||
None: If no agent ID is found or if "__main__" is encountered | ||
""" | ||
for frame_info in inspect.stack(): | ||
local_vars = frame_info.frame.f_locals | ||
|
||
for var_name, var in local_vars.items(): | ||
# Stop at main | ||
if var == "__main__": | ||
return None | ||
|
||
try: | ||
# Check if object has our AgentOpsDescriptor descriptors | ||
var_type = type(var) | ||
|
||
# Get all class attributes | ||
class_attrs = {name: getattr(var_type, name, None) for name in dir(var_type)} | ||
|
||
agent_id_desc = class_attrs.get("agentops_agent_id") | ||
|
||
if isinstance(agent_id_desc, agentops_property): | ||
agent_id = agent_id_desc.__get__(var, var_type) | ||
|
||
if agent_id: | ||
agent_name_desc = class_attrs.get("agentops_agent_name") | ||
if isinstance(agent_name_desc, agentops_property): | ||
agent_name = agent_name_desc.__get__(var, var_type) | ||
return agent_id | ||
except Exception: | ||
continue | ||
|
||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.