Source code for flexicon.code.Lists.AgentOperations

#
#   AgentOperations.py
#
#   Class: AgentOperations
#          Agent operations for managing analysis agents (parsers, human analysts)
#          in FieldWorks Language Explorer projects via SIL Language and Culture
#          Model (LCM) API.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

# Import FLEx LCM types
from SIL.LCModel import (
    ICmAgent,
    ICmAgentFactory,
    ICmPerson,
    ICmAgentEvaluation,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils
import System

# Import flexlibs exceptions
from ..FLExProject import FP_ParameterError, FP_NullParameterError
from ..BaseOperations import OperationsMethod
from .possibility_item_base import PossibilityItemOperations
from ..Shared.string_utils import normalize_match_key


[docs] class AgentOperations(PossibilityItemOperations): """ Agent operations for managing human analysts and automated parsers. Agents represent either human analysts or automated parsers that perform linguistic analysis. They track who (or what) created analyses, glosses, and evaluations in the project. Overridden CRUD Operations (AnalyzingAgentsOC has no PossibilitiesOS): - GetAll() - Get all agents (iterates OC directly) - Create() - Create new agent (uses ICmAgentFactory + OC.Add) - Delete() - Delete agent (uses OC.Remove) - Duplicate() - Clone agent (uses OC.Add) - Find() - Find by name (searches OC directly) Inherited Operations (from PossibilityItemOperations): - Exists() - Check existence - GetName() / SetName() - Get/set name - GetDescription() / SetDescription() - Get/set description - GetGuid() - Get GUID - CompareTo() - Compare by name Domain-Specific Methods (AgentOperations): - CreateHumanAgent() - Create human agent linked to person - CreateParserAgent() - Create parser agent with version - GetVersion() / SetVersion() - Get/set parser version - IsHuman() / IsParser() - Check agent type - GetHuman() / SetHuman() - Link to ICmPerson - GetEvaluations() / GetEvaluationCount() - Get evaluations - FindByType() - Find by agent type - GetHumanAgents() / GetParserAgents() - Get all of type - GetDateCreated() / GetDateModified() - Get timestamps """ def __init__(self, project): """ Initialize AgentOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) def _get_item_class_name(self): """Get the item class name for error messages.""" return "Agent" def _get_list_object(self): """Get the agents list container.""" return self.project.lp.AnalyzingAgentsOC @OperationsMethod def GetAll(self): """ Iterate over all agents in the project's AnalyzingAgentsOC. Overrides the PossibilityItemOperations.GetAll inherited default, which assumes the list-object has PossibilitiesOS. LangProject.AnalyzingAgentsOC is a plain LcmOwningCollection of ICmAgent, not a CmPossibilityList, so PossibilitiesOS doesn't exist on it -- AttributeError every call. Iterate the collection directly. Returns: list[ICmAgent]: All agents. Notes: - Returns ALL agents, including stale parser-version agents left behind when the parser is upgraded. Callers that want only human analysts should filter with:: [a for a in project.Agents.GetAll() if project.Agents.IsHuman(a)] or call GetHumanAgents() directly. """ agents_oc = self._get_list_object() if agents_oc is None: return [] return list(agents_oc) @OperationsMethod def Create(self, name, wsHandle=None, guid=None): """ Create a new agent in AnalyzingAgentsOC. Overrides PossibilityItemOperations.Create, which calls list_obj.PossibilitiesOS -- a property that does not exist on LcmOwningCollection (AnalyzingAgentsOC). This override uses ICmAgentFactory and adds the new object directly to the OC. Args: name (str): The name for the new agent. wsHandle: Optional writing system handle. Defaults to analysis WS. guid (optional): GUID to assign to the new agent, as a ``System.Guid`` or string. Use this when REPRODUCING an agent from another project so it keeps its original identity -- useful with the ``flexicon/sync/`` engine for preserving which agent evaluated an analysis across a transfer or merge. None (the default) mints a fresh GUID. Note that the 3 bootstrap agents (``kguidAgentDefUser``, ``kguidAgentXAmpleParser``, ``kguidAgentComputer``) already exist in every project, so passing one of their GUIDs will hit the duplicate-GUID fallback below. Returns: ICmAgent: The newly created agent object. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If name is None. FP_ParameterError: If name is empty, or if guid was supplied but is not a valid GUID. Notes: - If the requested guid is already present in the project (e.g. one of the 3 bootstrap agent GUIDs), creation falls back to a fresh identity and logs a warning; it does not raise. See Also: Duplicate, Find, Delete """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") if not name or not name.strip(): raise FP_ParameterError("Agent name cannot be empty") agents_oc = self._get_list_object() if agents_oc is None: raise FP_ParameterError("AnalyzingAgentsOC not found in project") if wsHandle is None: wsHandle = self.project.project.DefaultAnalWs factory = self.project.project.ServiceLocator.GetService(ICmAgentFactory) with self._TransactionCM(f"Create agent {name!r}"): new_agent = self._CreateWithGuid(factory, guid, "ICmAgent") # Add to collection before setting properties agents_oc.Add(new_agent) mkstr = TsStringUtils.MakeString(name, wsHandle) new_agent.Name.set_String(wsHandle, mkstr) return new_agent @OperationsMethod def Delete(self, agent_or_hvo): """ Delete an agent from AnalyzingAgentsOC. Overrides PossibilityItemOperations.Delete, which calls list_obj.PossibilitiesOS.Remove() -- not available on the OC. This override removes directly from AnalyzingAgentsOC. Args: agent_or_hvo: Either an ICmAgent object or its HVO. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If agent_or_hvo is None. """ self._EnsureWriteEnabled() self._ValidateParam(agent_or_hvo, "agent_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) agents_oc = self._get_list_object() if agents_oc is not None and agent in agents_oc: with self._TransactionCM("Delete agent"): agents_oc.Remove(agent) @OperationsMethod def Duplicate(self, agent_or_hvo, insert_after=True, deep=False): """ Duplicate an agent, creating a new copy with a new GUID. Overrides PossibilityItemOperations.Duplicate, which calls list_obj.PossibilitiesOS.Add/Insert -- not available on the OC. This override adds directly to AnalyzingAgentsOC and copies the Name and Human flag from the source. Args: agent_or_hvo: Either an ICmAgent object or its HVO to duplicate. insert_after (bool): Ignored for OC collections (no ordered insert); the duplicate is always appended. deep (bool): Accepted for API uniformity across Operations classes. Agent has no owned objects, so this parameter is ignored. Returns: ICmAgent: The newly created duplicate agent with a new GUID. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If agent_or_hvo is None. """ self._EnsureWriteEnabled() self._ValidateParam(agent_or_hvo, "agent_or_hvo") source = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) agents_oc = self._get_list_object() if agents_oc is None: raise FP_ParameterError("AnalyzingAgentsOC not found in project") factory = self.project.project.ServiceLocator.GetService(ICmAgentFactory) with self._TransactionCM("Duplicate agent"): duplicate = factory.Create() agents_oc.Add(duplicate) # Copy MultiString name alternatives duplicate.Name.CopyAlternatives(source.Name) # Copy Human flag (True = human analyst) duplicate.Human = source.Human return duplicate @OperationsMethod def Find(self, name): """ Find an agent by name (case-insensitive). Searches AnalyzingAgentsOC directly (via GetAll). Args: name (str): The name to search for. Returns: ICmAgent: The matching agent, or None if not found. Notes: - Searches ALL agents, including stale parser-version agents accumulated from parser upgrades. If you want only human agents, filter the result with IsHuman() or use GetHumanAgents() instead. """ self._ValidateParam(name, "name") if not name or not name.strip(): return None target = normalize_match_key(name, casefold=True).strip() wsHandle = self.project.project.DefaultAnalWs for agent in self.GetAll(): agent_name = ITsString(agent.Name.get_String(wsHandle)).Text if normalize_match_key(agent_name, casefold=True).strip() == target: return agent return None # --- Sync integration (ICmAgent is not ICmPossibility) --- @OperationsMethod def GetSyncableProperties(self, item_or_hvo): """Return syncable properties for an analyzing agent. ICmAgent exposes ``Name`` and ``Human`` but not ``Description``. The inherited ``PossibilityItemOperations.GetSyncableProperties`` unconditionally read ``Description`` and raised ``AttributeError`` on every agent (issue #350). """ self._ValidateParam(item_or_hvo, "item_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(item_or_hvo) props = {"Guid": str(agent.Guid)} name_alts = {} for ws in self.project.WritingSystems.GetAll(): wsHandle = ws.Handle name_str = agent.Name.get_String(wsHandle) if name_str: name_alts[str(wsHandle)] = ITsString(name_str).Text or "" if name_alts: props["Name"] = name_alts props["Human"] = bool(agent.Human) version = self.GetVersion(agent) if version: props["Version"] = version return props @OperationsMethod def GetDescription(self, item_or_hvo, wsHandle=None): """ICmAgent has no Description member (issue #350).""" self._ValidateParam(item_or_hvo, "item_or_hvo") return "" @OperationsMethod def SetDescription(self, item_or_hvo, description, wsHandle=None): """ICmAgent has no Description member; intentionally a no-op (#350).""" self._ValidateParam(item_or_hvo, "item_or_hvo") # --- Version and Type Management --- @OperationsMethod def GetVersion(self, agent_or_hvo, wsHandle=None): """ Get the version string of an agent. Args: agent_or_hvo: Either an ICmAgent object or its HVO wsHandle: Ignored. Accepted for backwards compatibility only -- ICmAgent.Version is a monolingual Unicode property, so there is no per-writing-system alternative to select. Returns: str: The agent's version string (empty string if not set) Raises: FP_NullParameterError: If agent_or_hvo is None Example: >>> parser = project.Agents.Find("MyParser") >>> version = project.Agents.GetVersion(parser) >>> print(f"Version: {version}") Version: 1.0.0 >>> # Human agents typically don't have versions >>> human = project.Agents.Find("John Smith") >>> version = project.Agents.GetVersion(human) >>> print(version) Notes: - Returns empty string if version not set - Typically used for parser agents - Human agents usually don't have version numbers - Can use any version format (e.g., "1.0", "2.1.3", "v3-beta") - ICmAgent.Version is a plain Unicode property in the LCM model (unlike ICmAgent.Name, which is MultiUnicode). pythonnet surfaces it as a Python str, so it must NOT be read through get_String()/ITsString -- doing so raised ``AttributeError: 'str' object has no attribute 'get_String'`` on every call. See Also: SetVersion, CreateParserAgent """ self._ValidateParam(agent_or_hvo, "agent_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) return agent.Version or "" @OperationsMethod def SetVersion(self, agent_or_hvo, version, wsHandle=None): """ Set the version string of an agent. Args: agent_or_hvo: Either an ICmAgent object or its HVO version (str): The version string (e.g., "1.0.0") wsHandle: Ignored. Accepted for backwards compatibility only -- ICmAgent.Version is a monolingual Unicode property, so there is no per-writing-system alternative to set. Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If agent_or_hvo or version is None Example: >>> parser = project.Agents.Find("MyParser") >>> project.Agents.SetVersion(parser, "1.0.1") >>> print(project.Agents.GetVersion(parser)) 1.0.1 >>> # Update to new version >>> project.Agents.SetVersion(parser, "2.0.0") >>> # Clear version >>> project.Agents.SetVersion(parser, "") Notes: - Typically used for parser agents - Can be empty string to clear - Use semantic versioning (e.g., "1.0.0") recommended - Update when parser algorithm changes - ICmAgent.Version is a plain Unicode property in the LCM model (unlike ICmAgent.Name, which is MultiUnicode). It is assigned directly as a Python str, not via set_String(). See Also: GetVersion, CreateParserAgent """ self._EnsureWriteEnabled() self._ValidateParam(agent_or_hvo, "agent_or_hvo") self._ValidateParam(version, "version") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) with self._TransactionCM("Set agent version"): agent.Version = version # --- Agent Type Operations --- @OperationsMethod def IsHuman(self, agent_or_hvo): """ Check if an agent is a human analyst. Args: agent_or_hvo: Either an ICmAgent object or its HVO Returns: bool: True if agent has a linked human (ICmPerson), False otherwise Raises: FP_NullParameterError: If agent_or_hvo is None Example: >>> for agent in project.Agents.GetAll(): ... name = project.Agents.GetName(agent) ... if project.Agents.IsHuman(agent): ... person = project.Agents.GetHuman(agent) ... print(f"Human: {name} ({project.Person.GetName(person)})") ... else: ... version = project.Agents.GetVersion(agent) ... print(f"Parser: {name} v{version}") Human: John Smith (john.smith@example.com) Parser: MyParser v1.0.0 Notes: - Returns True if agent.Human is set - Returns False for parser/computer agents - Human agents have associated ICmPerson objects See Also: IsParser, GetHuman, SetHuman, CreateHumanAgent """ self._ValidateParam(agent_or_hvo, "agent_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) # ICmAgent.Human is a Boolean property -- always non-None for a # valid agent -- so the previous `agent.Human is not None` # check returned True for every agent (parsers included), # silently merging the two types and causing # WfiAnalyses.SetApprovalStatus to pick parsers as "human" by # mistake. (surfaced while writing the #38 regression test) return bool(agent.Human) @OperationsMethod def IsParser(self, agent_or_hvo): """ Check if an agent is a parser (automated analyzer). Args: agent_or_hvo: Either an ICmAgent object or its HVO Returns: bool: True if agent has no linked human, False otherwise Raises: FP_NullParameterError: If agent_or_hvo is None Example: >>> parser = project.Agents.Find("MyParser") >>> if project.Agents.IsParser(parser): ... version = project.Agents.GetVersion(parser) ... print(f"Parser version: {version}") Parser version: 1.0.0 >>> # Filter only parsers >>> parsers = [a for a in project.Agents.GetAll() ... if project.Agents.IsParser(a)] >>> print(f"Found {len(parsers)} parsers") Notes: - Returns True if agent.Human is None - Opposite of IsHuman() - Parser agents represent automated analysis tools See Also: IsHuman, CreateParserAgent, GetVersion """ self._ValidateParam(agent_or_hvo, "agent_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) # See IsHuman: ICmAgent.Human is a Boolean, never None. return not bool(agent.Human) # --- Human (Person) Link --- @OperationsMethod def GetHuman(self, agent_or_hvo): """ Get the linked person for a human agent. Args: agent_or_hvo: Either an ICmAgent object or its HVO Returns: ICmPerson or None: The linked person object, or None if not set Raises: FP_NullParameterError: If agent_or_hvo is None Example: >>> agent = project.Agents.Find("John Smith") >>> if project.Agents.IsHuman(agent): ... person = project.Agents.GetHuman(agent) ... name = project.Person.GetName(person) ... print(name) John Smith >>> # Parser agents return None >>> parser = project.Agents.Find("MyParser") >>> person = project.Agents.GetHuman(parser) >>> print(person) None Notes: - Returns None for parser agents - Returns ICmPerson object for human agents - Person contains biographical and contact information See Also: SetHuman, IsHuman, CreateHumanAgent """ self._ValidateParam(agent_or_hvo, "agent_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) return agent.Human @OperationsMethod def SetHuman(self, agent_or_hvo, person): """ Set the linked person for an agent (making it a human agent). Args: agent_or_hvo: Either an ICmAgent object or its HVO person: ICmPerson object to link (or None to unlink) Raises: FP_ReadOnlyError: If project is not opened with write enabled FP_NullParameterError: If agent_or_hvo is None FP_ParameterError: If person is not a valid ICmPerson (when not None) Example: >>> # Convert parser to human agent >>> agent = project.Agents.Find("MyAgent") >>> person = project.Person.Find("John Smith") >>> project.Agents.SetHuman(agent, person) >>> # Unlink person (convert to parser) >>> project.Agents.SetHuman(agent, None) Notes: - Set to ICmPerson to make human agent - Set to None to convert to parser agent - Person must exist before linking - Human agents can have biographical data See Also: GetHuman, CreateHumanAgent, IsHuman """ self._EnsureWriteEnabled() self._ValidateParam(agent_or_hvo, "agent_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) # Resolve/validate the cast BEFORE opening the unit of work: a bad # `person` must raise without ever opening (and immediately rolling # back) an undo task. Both branches then mutate, so the bracket has # no no-op path to protect. (D5/P3 validate-then-mutate.) if person is None: person_obj = None else: try: person_obj = ICmPerson(person) except (TypeError, System.InvalidCastException, AttributeError) as e: raise FP_ParameterError(f"person must be a valid ICmPerson object: {e}") with self._TransactionCM("Set agent human analyst"): agent.Human = person_obj # --- Evaluations --- @OperationsMethod def GetEvaluations(self, agent_or_hvo): """ Get all evaluations created by an agent. Args: agent_or_hvo: Either an ICmAgent object or its HVO Returns: list: List of ICmAgentEvaluation objects (empty list if none) Raises: FP_NullParameterError: If agent_or_hvo is None Example: >>> agent = project.Agents.Find("MyParser") >>> evaluations = project.Agents.GetEvaluations(agent) >>> print(f"Parser created {len(evaluations)} evaluations") Parser created 145 evaluations >>> # Examine evaluations >>> for eval in evaluations[:5]: ... # Access evaluation properties ... print(f"Evaluation: {eval.Guid}") Notes: - Returns empty list if no evaluations - Evaluations link agents to analyses - Used to track who/what approved analyses - Both human and parser agents can have evaluations See Also: GetEvaluationCount """ self._ValidateParam(agent_or_hvo, "agent_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) # Get evaluations from the referring collection evaluations = [] try: # Access evaluations that reference this agent for eval_obj in self.project.project.ServiceLocator.ObjectRepository.AllInstances(ICmAgentEvaluation): if eval_obj.Owner == agent: evaluations.append(eval_obj) except (AttributeError, System.NullReferenceException, RuntimeError) as e: # If collection not accessible, return empty list pass return evaluations @OperationsMethod def GetEvaluationCount(self, agent_or_hvo): """ Get the number of evaluations created by an agent. Args: agent_or_hvo: Either an ICmAgent object or its HVO Returns: int: Number of evaluations (0 if none) Raises: FP_NullParameterError: If agent_or_hvo is None Example: >>> agent = project.Agents.Find("MyParser") >>> count = project.Agents.GetEvaluationCount(agent) >>> print(f"Parser has {count} evaluations") Parser has 145 evaluations >>> # Compare human vs parser evaluations >>> for agent in project.Agents.GetAll(): ... name = project.Agents.GetName(agent) ... count = project.Agents.GetEvaluationCount(agent) ... agent_type = "Human" if project.Agents.IsHuman(agent) else "Parser" ... print(f"{name} ({agent_type}): {count} evaluations") Notes: - More efficient than len(GetEvaluations()) - Returns 0 if no evaluations - Useful for statistics and reporting See Also: GetEvaluations """ self._ValidateParam(agent_or_hvo, "agent_or_hvo") return len(self.GetEvaluations(agent_or_hvo)) # --- Query Methods --- @OperationsMethod def FindByType(self, is_human): """ Find all agents of a specific type (human or parser). Args: is_human (bool): True to find human agents, False for parsers Returns: list: List of ICmAgent objects matching the type Example: >>> # Get all human agents >>> humans = project.Agents.FindByType(True) >>> for agent in humans: ... name = project.Agents.GetName(agent) ... person = project.Agents.GetHuman(agent) ... print(f"{name}: {project.Person.GetName(person)}") >>> # Get all parsers >>> parsers = project.Agents.FindByType(False) >>> for parser in parsers: ... name = project.Agents.GetName(parser) ... version = project.Agents.GetVersion(parser) ... print(f"{name} v{version}") Notes: - Returns list, not iterator - Human agents have linked ICmPerson - Parser agents have no linked person - Empty list if none found See Also: GetHumanAgents, GetParserAgents, IsHuman, IsParser """ agents = [] for agent in self.GetAll(): if is_human: if self.IsHuman(agent): agents.append(agent) else: if self.IsParser(agent): agents.append(agent) return agents @OperationsMethod def GetHumanAgents(self): """ Get all human agents in the project. Returns: list: List of ICmAgent objects representing human analysts Example: >>> humans = project.Agents.GetHumanAgents() >>> print(f"Found {len(humans)} human agents") Found 3 human agents >>> for agent in humans: ... name = project.Agents.GetName(agent) ... person = project.Agents.GetHuman(agent) ... print(f"Analyst: {name} ({project.Person.GetName(person)})") Analyst: John Smith (john.smith@example.com) Analyst: Maria Garcia (maria.garcia@example.com) Analyst: Ahmed Hassan (ahmed.hassan@example.com) Notes: - Returns only agents with linked ICmPerson - Useful for identifying human annotators - Returns list, not iterator - Empty list if no human agents See Also: GetParserAgents, FindByType, IsHuman """ return self.FindByType(True) @OperationsMethod def GetParserAgents(self): """ Get all parser agents in the project. Returns: list: List of ICmAgent objects representing automated parsers Example: >>> parsers = project.Agents.GetParserAgents() >>> print(f"Found {len(parsers)} parsers") Found 2 parsers >>> for parser in parsers: ... name = project.Agents.GetName(parser) ... version = project.Agents.GetVersion(parser) ... count = project.Agents.GetEvaluationCount(parser) ... print(f"{name} v{version}: {count} evaluations") MyParser v1.0.0: 145 evaluations AutoAnalyzer v2.1.3: 89 evaluations Notes: - Returns only agents without linked ICmPerson - Useful for identifying automated analysis tools - Returns list, not iterator - Empty list if no parser agents See Also: GetHumanAgents, FindByType, IsParser """ return self.FindByType(False) # --- Metadata --- @OperationsMethod def GetDateCreated(self, agent_or_hvo): """ Get the creation date of an agent record. Args: agent_or_hvo: Either an ICmAgent object or its HVO Returns: System.DateTime: The date and time the agent record was created Raises: FP_NullParameterError: If agent_or_hvo is None Example: >>> agent = project.Agents.Find("MyParser") >>> created = project.Agents.GetDateCreated(agent) >>> print(f"Created: {created}") Created: 2025-01-15 14:30:22 >>> # Find oldest agent >>> agents = list(project.Agents.GetAll()) >>> oldest = min(agents, key=lambda a: project.Agents.GetDateCreated(a)) >>> print(f"Oldest: {project.Agents.GetName(oldest)}") Notes: - Returns System.DateTime object (not Python datetime) - Automatically set when agent is created - Cannot be modified (read-only property) - Timezone is local to the FLEx project See Also: GetDateModified """ self._ValidateParam(agent_or_hvo, "agent_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) return agent.DateCreated @OperationsMethod def GetDateModified(self, agent_or_hvo): """ Get the last modification date of an agent record. Args: agent_or_hvo: Either an ICmAgent object or its HVO Returns: System.DateTime: The date and time the agent record was last modified Raises: FP_NullParameterError: If agent_or_hvo is None Example: >>> agent = project.Agents.Find("MyParser") >>> modified = project.Agents.GetDateModified(agent) >>> print(f"Last modified: {modified}") Last modified: 2025-01-20 09:15:43 >>> # Find recently modified agents >>> from datetime import datetime, timedelta >>> # Note: System.DateTime, not Python datetime >>> for agent in project.Agents.GetAll(): ... modified = project.Agents.GetDateModified(agent) ... name = project.Agents.GetName(agent) ... print(f"{name}: {modified}") Notes: - Returns System.DateTime object (not Python datetime) - Automatically updated when agent changes - Cannot be modified directly (read-only property) - Updates on any change to agent properties See Also: GetDateCreated """ self._ValidateParam(agent_or_hvo, "agent_or_hvo") agent = self._PossibilityItemOperations__ResolveObject(agent_or_hvo) return agent.DateModified