As part of the Relativity Services API (RSAPI) Deprecation, content on this page referring to the RSAPI and the Patient Tracker application is in the process of being deprecated and will no longer be supported. For more information and alternative APIs, see RSAPI deprecation process.

Advanced agent code sample

You can develop agents that perform specialized tasks, such as querying a queue table for jobs or setting field values. This advanced sample code illustrates how an agent interacts with the PatientProcessingQueue table. It uses the Relativity Services API to query for patient data. It also shows how to use the client-side proxy to connect to the Services API. To download the Patient Tracker application that uses this custom code or to view it in a Visual Studio solution, see Patient Tracker application.

Copy
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Text.RegularExpressions;

namespace PatientTracker.Agents
{
    /// <summary>
    /// This agent queries the PatientProcessingQueue table in the EDDS database for patients that need to be processed in workspaces.
    /// After picking up a job from the queue, the agent uses the Services API to query for more metadata in the workspace.
    ///It also determines if the patient notes contains specific symptoms. It then sets a field indicating whether the patient requires further evaluation.
    /// </summary>
    [kCura.Agent.CustomAttributes.Name("Patient Notes Processing Agent")]
    [System.Runtime.InteropServices.Guid("3dc132a6-ba0f-448a-bc10-1667fa04b7cd")]
    public class PatientNotesProcessingAgent : kCura.Agent.AgentBase
    {

        public static readonly String[] SYMPTOMS_TO_FIND = new String[] { "bumps", "fever", "lethargic", "fatigued", "rash", "cough", "headache" };

        public static readonly Guid PATIENT_NOTES_FIELD_GUID = new Guid("5702F31B-48AC-4E2A-9000-A48B3E3ABDBF");
        public static readonly Guid REQUIRES_FURTHER_EVALUATION_FIELD_GUID = new Guid("4B247F25-966D-4BFB-AD6F-2647D93B7DA4");
        public static readonly Guid PATIENT_RDO_GUID = new Guid("EDC3AF33-7D25-4897-8285-6E36EA1D4AAE");

        private static Int32 ErrorCount { get; set; }

        public override void Execute()
        {
            PatientJobDTO patientToProcessJobDTO = null;
            Int32 keySymptomsFound;

            //Get the current Agent artifactID.
            Int32 agentArtifactID = this.AgentID;
            //Get the EDDS database context.
            Relativity.API.IDBContext eddsDBContext = this.Helper.GetDBContext(-1);

            try
            {
                //Get the next available job from the queue.
                this.RaiseMessage("Looking for the next job in the queue", 10);
                patientToProcessJobDTO = GetNextPatientJob(eddsDBContext, agentArtifactID);

                //If a job needs processing, enter the loop and start processing work until there are no open jobs left.
                while (patientToProcessJobDTO != null)
                {
                    this.RaiseMessage(String.Format("Beginning work on Patient ArtifactID: {0} in Workspace: {1}", patientToProcessJobDTO.PatientArtifactID, patientToProcessJobDTO.WorkspaceArtifactID), 10);

                    //Reset the key symptoms found count.
                    keySymptomsFound = 0;
                    String patientNotes = null;

                    //Construct an instance of RSAPIClient using the service account identity.
                    using (kCura.Relativity.Client.IRSAPIClient proxy = this.Helper.GetServicesManager().CreateProxy<kCura.Relativity.Client.IRSAPIClient>(Relativity.API.ExecutionIdentity.System))
                    {
                        //Set the workspace artifactID from the job.
                        proxy.APIOptions.WorkspaceID = patientToProcessJobDTO.WorkspaceArtifactID;

                        //Create the patientRDO to be populated with the patient artifactID.
                        kCura.Relativity.Client.DTOs.RDO patientRDOInstance = new kCura.Relativity.Client.DTOs.RDO(patientToProcessJobDTO.PatientArtifactID);
                        //Set the artifact type GUID of the Patient Object Type.
                        patientRDOInstance.ArtifactTypeGuids.Add(PATIENT_RDO_GUID);
                        //Add the fields that you want pulled back during the read operation.
                        patientRDOInstance.Fields.Add(new kCura.Relativity.Client.DTOs.FieldValue(PATIENT_NOTES_FIELD_GUID));


                        //Get the patient notes for the patient to be processed.
                        patientNotes = proxy.Repositories.RDO.Read(patientRDOInstance) //Perform the read operation
                                       .Results[0] //Get the first item in the results array
                                       .Artifact //Get the base artifact
                                       .Fields.Where(field => field.Guids.Contains(PATIENT_NOTES_FIELD_GUID)).Single() //Get the field from the Fields collection
                                       .ValueAsLongText; //Return the result as a string
                    }

                    this.RaiseMessage("Beginning searching for key symptoms", 1);
                    //In the patient notes, look for the key symptoms in the text.
                    //Loop through each key symptom in the array.
                    for (Int32 i = 0; i < SYMPTOMS_TO_FIND.Length; i++)
                    {
                        String currentSymptom = SYMPTOMS_TO_FIND[i];

                        //Run a regex expression to determine if there are any matches for this symptom.
                        Match match = Regex.Match(patientNotes, currentSymptom, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
                        if (match.Success == true)
                        {
                            keySymptomsFound += 1;
                            //If you find three of the key symptoms, break out of the loop.
                            if (keySymptomsFound == 3)
                            {
                                break;
                            }
                        }
                    }
                    Boolean requiresFurtherEvaluation = false;

                    //If multiple key symptoms have been found in the patient notes, set the Requires Further Evaluation field.
                    if (keySymptomsFound >= 3)
                    {
                        requiresFurtherEvaluation = true;
                    }

                    this.RaiseMessage(String.Format("Updating Requires Further Evaluation field for Patient ArtifactID: {0} in Workspace: {1}", patientToProcessJobDTO.PatientArtifactID, patientToProcessJobDTO.WorkspaceArtifactID), 10);
                    using (kCura.Relativity.Client.IRSAPIClient client = this.Helper.GetServicesManager().CreateProxy<kCura.Relativity.Client.IRSAPIClient>(Relativity.API.ExecutionIdentity.System))
                    {
                        client.APIOptions.WorkspaceID = patientToProcessJobDTO.WorkspaceArtifactID;
                        //Create a new patientRDO to update.
                        kCura.Relativity.Client.DTOs.RDO patientToUpdate = new kCura.Relativity.Client.DTOs.RDO(patientToProcessJobDTO.PatientArtifactID);
                        //Set the artifact type GUID of the Patient Object Type.
                        patientToUpdate.ArtifactTypeGuids.Add(PATIENT_RDO_GUID);
                        //Add the fields that you want updated during the request.
                        patientToUpdate.Fields.Add(new kCura.Relativity.Client.DTOs.FieldValue(REQUIRES_FURTHER_EVALUATION_FIELD_GUID, requiresFurtherEvaluation));

                        //Update the record in the workspace.
                        client.Repositories.RDO.UpdateSingle(patientToUpdate);
                    }

                    this.RaiseMessage(String.Format("Completed work on Patient ArtifactID: {0} in Workspace: {1}", patientToProcessJobDTO.PatientArtifactID, patientToProcessJobDTO.WorkspaceArtifactID), 10);
                    //Delete the job from the database.
                    DeleteJob(eddsDBContext, patientToProcessJobDTO);

                    //Get the next job from the queue if any exist.
                    patientToProcessJobDTO = GetNextPatientJob(eddsDBContext, agentArtifactID);
                }
            }
            catch (System.Exception ex)
            {
                //If an exception occurred while processing a job, update the status and error message of the job.
                if (patientToProcessJobDTO != null)
                {
                    patientToProcessJobDTO.Status = JobStatus.Error;
                    patientToProcessJobDTO.ErrorMessage = ex.ToString();
                    UpdateJobStatus(eddsDBContext, patientToProcessJobDTO);
                }

                //Increment the error count.
                ErrorCount += 1;
                //If the agent has failed 5 times, a serious issue may have occurred. Raise an error and shut it down.
                if (ErrorCount >= 5)
                {
                    RaiseError("Agent is shutting down because it has reached the error limit", ex.ToString());
                }
            }
        }
        /// <summary>
        /// Return the name of the agent that appears in the debugging Winform application only.
        /// Use description class attribute to display the description in Relativity.
        /// </summary>
        public override string Name
        {
            get
            {
                return "Patient Notes Processing Agent";
            }
        }

        /// <summary>
        /// Updates the job row in the PatientProcessingQueue table in the EDDS database.
        /// </summary>
        /// <param name="eddsDBContext">A properly constructed EDDS database context</param>
        /// <param name="patientJobDTO">The job DTO to be updated</param>
        private static void UpdateJobStatus(Relativity.API.IDBContext eddsDBContext, PatientJobDTO patientJobDTO)
        {
            String updateSQL = @"
                                UPDATE [PatientProcessingQueue]
                                SET [Status] = @status,
                                     [ErrorMessage] = @errorMessage,
                                     [AgentArtifactID] = NULL
                                WHERE [PatientArtifactID] = @patientArtifactID
                                AND [WorkspaceArtifactID] = @workspaceArtifactID
                                                                              ";
            SqlParameter patientArtifactIDParam = new SqlParameter("@patientArtifactID", SqlDbType.Int);
            patientArtifactIDParam.Value = patientJobDTO.PatientArtifactID;
            SqlParameter workspaceArtifactIDParam = new SqlParameter("@workspaceArtifactID", SqlDbType.Int);
            workspaceArtifactIDParam.Value = patientJobDTO.WorkspaceArtifactID;
            SqlParameter statusParam = new SqlParameter("@status", SqlDbType.Int);
            statusParam.Value = (Int32)patientJobDTO.Status;
            SqlParameter errorMessageParam = new SqlParameter("@errorMessage", SqlDbType.NVarChar);
            errorMessageParam.Value = (object)patientJobDTO.ErrorMessage ?? DBNull.Value;

            eddsDBContext.ExecuteNonQuerySQLStatement(updateSQL, new SqlParameter[] { patientArtifactIDParam, workspaceArtifactIDParam, statusParam, errorMessageParam });
        }

        /// <summary>
        /// Queries the PatientProcessingQueue table in the EDDS database to find any jobs that are already assigned to this agent, or gets the next available job ordered by ascending ID. 
        /// </summary>
        /// <param name="eddsDBContext">A properly constructed SQL database context for the EDDS database</param>
        /// <param name="agentArtifactID">ArtifactID for the current agent that is executing</param>
        /// <returns>A properly constructed PatientJobDTO or null if there are no jobs to be processed</returns>
        private static PatientJobDTO GetNextPatientJob(Relativity.API.IDBContext eddsDBContext, Int32 agentArtifactID)
        {
            PatientJobDTO retVal = null;

            String nextJobSQL = @"      DECLARE @nextJobID INT

                        BEGIN TRAN Tran1
                             SET @nextJobID = (SELECT TOP 1 [ID] FROM [PatientProcessingQueue] WITH(UPDLOCK, READPAST) WHERE [AgentArtifactID] = @agentArtifactID AND [Status] = 1)

                              IF (@nextJobID IS NULL)
                                 BEGIN
                                 SET @nextJobID = (SELECT TOP 1 [ID] FROM [PatientProcessingQueue] WITH(UPDLOCK, READPAST) WHERE [AgentArtifactID] IS NULL AND [Status] = 0 ORDER BY [ID] ASC)

                                  UPDATE [PatientProcessingQueue]
                                  SET [AgentArtifactID] = @agentArtifactID, [Status] = 1
                                  WHERE [ID] = @nextJobID
                                       END
                                       COMMIT TRAN
                                       SELECT * FROM [PatientProcessingQueue] WHERE [ID] = @nextJobID";

            SqlParameter agentArtifactIDParam = new SqlParameter("@agentArtifactID", SqlDbType.Int);
            agentArtifactIDParam.Value = agentArtifactID;

            //Query the database for the next job.
            DataTable nextJobTable = eddsDBContext.ExecuteSqlStatementAsDataTable(nextJobSQL, new SqlParameter[] { agentArtifactIDParam });

            //If the query returns any rows, create a DTO to return.
            if (nextJobTable.Rows.Count > 0)
            {
                DataRow nextJobRow = nextJobTable.Rows[0];
                retVal = new PatientJobDTO();
                retVal.PatientArtifactID = Convert.ToInt32(nextJobRow["PatientArtifactID"]);
                retVal.WorkspaceArtifactID = Convert.ToInt32(nextJobRow["WorkspaceArtifactID"]);
                retVal.Status = (JobStatus)Convert.ToInt32(nextJobRow["Status"]);
            }
            return retVal;
        }

        /// <summary>
        /// Delete the patient job from the PatientProcessingQueue table in the EDDS database.
        /// </summary>
        /// <param name="eddsDBContext">A properly constructed SQL database context for the EDDS database.</param>
        /// <param name="patientJobDTO">The patient job DTO to be deleted.</param>
        private static void DeleteJob(Relativity.API.IDBContext eddsDBContext, PatientJobDTO patientJobDTO)
        {
            String deleteSQL = "DELETE FROM [PatientProcessingQueue] WHERE [PatientArtifactID] = @patientArtifactID AND [WorkspaceArtifactID] = @workspaceArtifactID";
            SqlParameter patientArtifactIDParam = new SqlParameter("@patientArtifactID", SqlDbType.Int);
            patientArtifactIDParam.Value = patientJobDTO.PatientArtifactID;
            SqlParameter workspaceArtifactIDParam = new SqlParameter("@workspaceArtifactID", SqlDbType.Int);
            workspaceArtifactIDParam.Value = patientJobDTO.WorkspaceArtifactID;
            eddsDBContext.ExecuteNonQuerySQLStatement(deleteSQL, new SqlParameter[] { patientArtifactIDParam, workspaceArtifactIDParam });
        }
    }

    public class PatientJobDTO
    {
        public Int32 PatientArtifactID { get; set; }
        public Int32 WorkspaceArtifactID { get; set; }
        public JobStatus Status { get; set; }
        public String ErrorMessage { get; set; }
    }

    public enum JobStatus
    {
        NotStarted = 0,
        InProgress = 1,
        Error = 2
    }
}