Integration of DSPy with tm LLM agent 79/14979/15
authorggori <wjdxodlf012345@khu.ac.kr>
Thu, 25 Sep 2025 08:04:18 +0000 (08:04 +0000)
committerggori <wjdxodlf012345@khu.ac.kr>
Sun, 28 Sep 2025 12:33:12 +0000 (12:33 +0000)
- Dockerfile
Add build-essential to Dockerfile for native extention compilation
- service
Implement the DSPy-based LLM agent as singleton class that initializes with API key and the model, and processes user requests in natural language.
- controller
Recieves API requests, forwards them to service agent, and returns the processed results.

Issue-Id: AIMLFW-249

Change-Id: I92ed260ef216de2e272e202c4584da75c77e5d0f
Signed-off-by: ggori <wjdxodlf012345@khu.ac.kr>
Dockerfile
requirements.txt
tox.ini
trainingmgr/controller/agent_controller.py
trainingmgr/service/agent_service.py [new file with mode: 0644]

index 77eee87..a6ad69b 100644 (file)
@@ -7,7 +7,7 @@ WORKDIR /app
 
 # Install dependencies
 RUN apt-get update && \
-    apt-get install -y --no-install-recommends python3-pip apt-utils git && \
+    apt-get install -y --no-install-recommends python3-pip apt-utils git build-essential && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
 
index f9fd5d0..c1f10ef 100644 (file)
@@ -32,3 +32,4 @@ marshmallow==3.26.1
 psycopg2-binary==2.9.10
 marshmallow-sqlalchemy
 flask-marshmallow
+dspy==3.0.3
diff --git a/tox.ini b/tox.ini
index 240a2e6..f1b0c32 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -47,6 +47,7 @@ deps=
   marshmallow-sqlalchemy
   flask-marshmallow
   psycopg2-binary==2.9.10
+  dspy==3.0.3
 
 setenv = cd  = {toxinidir}/tests
 commands =
index d9b5fc1..e7e69c3 100644 (file)
 #
 # ==================================================================================
 from flask import Blueprint, request, jsonify
+from trainingmgr.service.agent_service import AgentClient
 
 agent_controller = Blueprint("agent_controller", __name__)
 
+# Singleton instance of AgentClient
+_agent_client = AgentClient()
+
+# Initialize the agent once when the module is loaded
+_agent_client.initialize_agent()
+
 @agent_controller.route("/modelInfo", methods=["GET"])
 def model_info():
     return jsonify({
         "llm": {
-            "model": ""
+            "model": "",
         }
     }), 200
 
@@ -31,17 +38,38 @@ def model_info():
 def generate_content():
     body = request.get_json(silent=True) or {}
     text = body.get("text")
+    
     if not isinstance(text, str) or not text.strip():
         return jsonify({
             "title": "Bad Request",
             "status": 400,
             "detail": "The 'text' field is required and must be a non-empty string."
         }), 400
-    dry_run = bool(body.get("dry_run", True))
-    return jsonify({
-        "action": "noop",
-        "request": {"text": text, "dry_run": dry_run},
-        "response": {"note": "Received successfully"},
-        "status": "ok",
-        "error_message": None
-    }), 200
\ No newline at end of file
+        
+    try:
+        result = _agent_client.process_user_request(text)
+        if result['success']:
+            return jsonify({
+                "action": "completed",
+                "request": {"text": text},
+                "response": {"result": result['result']},
+                "status": "ok",
+                "error_message": None
+            }), 200
+        else:
+            return jsonify({
+                "action": "failed",
+                "request": {"text": text},
+                "response": {"error": result['error']},
+                "status": "error",
+                "error_message": result['error']
+            }), 500
+        
+    except Exception as err:
+        return jsonify({
+            "action": "failed",
+            "request": {"text": text},
+            "response": {"error": str(err)},
+            "status": "error",
+            "error_message": str(err)
+        }), 500
\ No newline at end of file
diff --git a/trainingmgr/service/agent_service.py b/trainingmgr/service/agent_service.py
new file mode 100644 (file)
index 0000000..d1aacc8
--- /dev/null
@@ -0,0 +1,103 @@
+# ==================================================================================
+#
+#       Copyright (c) 2025 Taeil Jung <wjdxodlf0123@gmail.com> 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.
+#
+# ==================================================================================
+import dspy
+import os
+from trainingmgr.common.trainingmgr_config import TrainingMgrConfig
+from trainingmgr.common.exceptions_utls import TMException
+from threading import Lock
+
+LOGGER = TrainingMgrConfig().logger
+
+# Define the agent signature
+class AgentSignature(dspy.Signature):
+    """A signature for the DSPy agent."""
+    query: str = dspy.InputField(desc= "The user's natural language request for creating feature group or registering model")
+    final: str = dspy.OutputField(desc= "Message that summarize the process result")
+
+
+class AgentClient:
+    """Encapsulates the DSPy agent. Implements Singleton pattern if desired."""
+
+    _instance = None
+    _lock = Lock()
+
+    def __new__(cls, *args, **kwargs):
+        """Singleton: ensure only one instance is created."""
+        if cls._instance is None:
+            with cls._lock:
+                if cls._instance is None:
+                    cls._instance = super().__new__(cls, *args, **kwargs)
+        return cls._instance
+    
+    def __init__(self):
+       self._agent = None
+       self._initialized = False
+
+    def initialize_agent(self) -> bool:
+        """Initialize the DSPy agent with tools."""
+        if self._initialized:
+            return True
+
+        try:
+            agent_model = os.getenv("LLM_AGENT_MODEL_FOR_TM")
+            agent_token = os.getenv("LLM_AGENT_MODEL_TOKEN_FOR_TM")
+
+            if not agent_model:
+                LOGGER.error("LLM_AGENT_MODEL_FOR_TM not specified")
+                return False
+            elif not agent_token:
+                LOGGER.error("LLM_AGENT_MODEL_TOKEN_FOR_TM not found")
+                return False 
+
+            # LM configuration
+            lm = dspy.LM(agent_model, api_key=agent_token)
+            dspy.configure(lm=lm)
+
+            # Agent configuration
+            self._agent = dspy.ReAct(
+                AgentSignature,
+                tools=[],
+                max_iters=6
+            )
+
+            self._initialized = True
+            LOGGER.info("Agent initialized successfully.")
+            return True
+
+        except Exception as err:
+            raise TMException(f"fail to initialize agent exception : {str(err)}")
+
+    def process_user_request(self, user_text_request):
+        """Process user request with agent tools."""
+        if not self._initialized or self._agent is None:
+            raise TMException("Agent not initialized")
+
+        try:
+            result = self._agent(query=user_text_request)
+            response = result.final
+            return {
+                'success': True,
+                'result': response,
+            }
+        except Exception as err:
+            LOGGER.error(f"Error processing user request: {str(err)}")
+            return {
+                'success': False,
+                'error': str(err),
+            }
+