From 55efc8c6453671a5b2d96afbe5fcbeb7c235e9ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:52:26 +0000 Subject: [PATCH 1/5] Initial plan for issue From cbe01bd82ab8b33233056472c856932d2d7c84e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:10:33 +0000 Subject: [PATCH 2/5] Complete Python agent implementation with full API integration Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> --- python-agent/README.md | 178 +++++++++++++++++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 164 bytes .../agents/__pycache__/base.cpython-312.pyc | Bin 0 -> 3649 bytes python-agent/agents/base.py | 82 +++++++- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 174 bytes .../__pycache__/sql_agent.cpython-312.pyc | Bin 0 -> 5831 bytes python-agent/agents/sql_agent/config.yaml | 26 +++ python-agent/agents/sql_agent/questions.yaml | 5 + python-agent/agents/sql_agent/sql_agent.py | 162 +++++++++++++--- python-agent/config.yaml | 20 ++ python-agent/main.py | 50 ++++- python-agent/requirements.txt | 8 +- python-agent/services/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 166 bytes .../agent_client_service.cpython-312.pyc | Bin 0 -> 8444 bytes .../__pycache__/config.cpython-312.pyc | Bin 0 -> 6868 bytes .../__pycache__/key_service.cpython-312.pyc | Bin 0 -> 5012 bytes .../keycloak_service.cpython-312.pyc | Bin 0 -> 7336 bytes .../sentrius_agent.cpython-312.pyc | Bin 0 -> 8654 bytes python-agent/services/agent_client_service.py | 178 +++++++++++++++++ python-agent/services/config.py | 133 +++++++++++++ python-agent/services/key_service.py | 107 ++++++++++ python-agent/services/keycloak_service.py | 138 +++++++++++++ python-agent/services/sentrius_agent.py | 163 ++++++++++++++++ python-agent/test.db | 0 python-agent/tests/__init__.py | 1 + python-agent/tests/test_integration.py | 182 ++++++++++++++++++ python-agent/tests/test_services.py | 136 +++++++++++++ 28 files changed, 1530 insertions(+), 40 deletions(-) create mode 100644 python-agent/README.md create mode 100644 python-agent/agents/__pycache__/__init__.cpython-312.pyc create mode 100644 python-agent/agents/__pycache__/base.cpython-312.pyc create mode 100644 python-agent/agents/sql_agent/__pycache__/__init__.cpython-312.pyc create mode 100644 python-agent/agents/sql_agent/__pycache__/sql_agent.cpython-312.pyc create mode 100644 python-agent/agents/sql_agent/config.yaml create mode 100644 python-agent/agents/sql_agent/questions.yaml create mode 100644 python-agent/config.yaml create mode 100644 python-agent/services/__init__.py create mode 100644 python-agent/services/__pycache__/__init__.cpython-312.pyc create mode 100644 python-agent/services/__pycache__/agent_client_service.cpython-312.pyc create mode 100644 python-agent/services/__pycache__/config.cpython-312.pyc create mode 100644 python-agent/services/__pycache__/key_service.cpython-312.pyc create mode 100644 python-agent/services/__pycache__/keycloak_service.cpython-312.pyc create mode 100644 python-agent/services/__pycache__/sentrius_agent.cpython-312.pyc create mode 100644 python-agent/services/agent_client_service.py create mode 100644 python-agent/services/config.py create mode 100644 python-agent/services/key_service.py create mode 100644 python-agent/services/keycloak_service.py create mode 100644 python-agent/services/sentrius_agent.py create mode 100644 python-agent/test.db create mode 100644 python-agent/tests/__init__.py create mode 100644 python-agent/tests/test_integration.py create mode 100644 python-agent/tests/test_services.py diff --git a/python-agent/README.md b/python-agent/README.md new file mode 100644 index 00000000..9cdaae2c --- /dev/null +++ b/python-agent/README.md @@ -0,0 +1,178 @@ +# Sentrius Python Agent + +This Python agent provides the same APIs and operations as the Java agent, enabling integration with the Sentrius platform for authentication, registration, heartbeat monitoring, and provenance event submission. + +## Features + +- **Keycloak Integration**: Full authentication support using Keycloak JWT tokens +- **Agent Registration**: Automatic registration with the Sentrius API server +- **Heartbeat Monitoring**: Continuous heartbeat mechanism to maintain connection +- **Provenance Events**: Submit detailed provenance events for audit trails +- **RSA Encryption**: Secure communication using ephemeral RSA keys +- **Configurable**: Support for both YAML configuration files and environment variables +- **Extensible**: Base agent framework for creating custom agents + +## Architecture + +The Python agent mirrors the Java agent architecture with these key components: + +### Services +- **KeycloakService**: Handles authentication and token management +- **AgentClientService**: Manages API communication with Sentrius server +- **EphemeralKeyGen**: RSA key generation and cryptographic operations +- **SentriusAgent**: Main agent framework coordinating all services + +### Agent Framework +- **BaseAgent**: Abstract base class for all agents +- **SQLAgent**: Example implementation for SQL operations + +## Configuration + +### YAML Configuration +```yaml +keycloak: + server_url: "http://localhost:8080" + realm: "sentrius" + client_id: "python-agent" + client_secret: "your-client-secret" + +agent: + name_prefix: "python-agent" + agent_type: "python" + callback_url: "http://localhost:8081" + api_url: "http://localhost:8080" + heartbeat_interval: 30 + +llm: + enabled: false + provider: "openai" + model: "gpt-3.5-turbo" + api_key: null + endpoint: null +``` + +### Environment Variables +```bash +KEYCLOAK_SERVER_URL=http://localhost:8080 +KEYCLOAK_REALM=sentrius +KEYCLOAK_CLIENT_ID=python-agent +KEYCLOAK_CLIENT_SECRET=your-client-secret +AGENT_NAME_PREFIX=python-agent +AGENT_API_URL=http://localhost:8080 +AGENT_CALLBACK_URL=http://localhost:8081 +AGENT_HEARTBEAT_INTERVAL=30 +``` + +## Usage + +### Running the SQL Agent +```bash +# With configuration file +python main.py sql_agent --config config.yaml + +# With default configuration (uses environment variables) +python main.py sql_agent +``` + +### Creating Custom Agents +```python +from agents.base import BaseAgent + +class MyCustomAgent(BaseAgent): + def __init__(self, config_path=None): + super().__init__("My Custom Agent", config_path=config_path) + + def execute_task(self): + # Your custom agent logic here + self.submit_provenance( + event_type="CUSTOM_TASK", + details={"task": "custom_operation"} + ) +``` + +## API Operations + +The Python agent supports all the same API operations as the Java agent: + +### Agent Registration +- **Endpoint**: `POST /api/v1/agent/register` +- **Purpose**: Register the agent with the Sentrius API server +- **Authentication**: Keycloak JWT token required + +### Heartbeat +- **Endpoint**: `POST /api/v1/agent/heartbeat` +- **Purpose**: Send periodic status updates to maintain connection +- **Frequency**: Configurable (default: 30 seconds) + +### Provenance Submission +- **Endpoint**: `POST /api/v1/agent/provenance/submit` +- **Purpose**: Submit detailed provenance events for audit trails +- **Data**: Event type, timestamp, agent ID, and custom details + +## Dependencies + +- `requests`: HTTP client for API communication +- `PyJWT`: JWT token handling +- `cryptography`: RSA key generation and encryption +- `pyyaml`: YAML configuration parsing +- `langchain`: LLM integration (for SQL agent) + +## Installation + +```bash +pip install -r requirements.txt +``` + +## Testing + +Run the test suite: +```bash +python tests/test_services.py +``` + +## Security + +- Uses ephemeral RSA key pairs for secure communication +- Validates JWT tokens using Keycloak public keys +- Supports encrypted data exchange with the API server +- Maintains secure token management throughout agent lifecycle + +## Integration with Java Ecosystem + +This Python agent is designed to work seamlessly with the existing Java-based Sentrius infrastructure: + +- Compatible with the same API endpoints +- Uses identical authentication mechanisms +- Submits provenance events in the same format +- Supports the same agent lifecycle management +- Can be launched using the same agent launcher service + +## Example Provenance Events + +The agent automatically submits various provenance events: + +```json +{ + "event_type": "AGENT_REGISTRATION", + "timestamp": "2024-01-01T12:00:00.000Z", + "agent_id": "python-agent-abc123", + "details": { + "agent_id": "python-agent-abc123", + "callback_url": "http://localhost:8081", + "agent_type": "python" + } +} +``` + +```json +{ + "event_type": "SQL_QUERY_SUCCESS", + "timestamp": "2024-01-01T12:01:00.000Z", + "agent_id": "python-agent-abc123", + "details": { + "question_number": 1, + "question": "What are the top 5 customers by revenue?", + "response_length": 245 + } +} +``` \ No newline at end of file diff --git a/python-agent/agents/__pycache__/__init__.cpython-312.pyc b/python-agent/agents/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c33e708d82b9a11964d83bfe2550358ce7d765e8 GIT binary patch literal 164 zcmX@j%ge<81RLf@Wq|0%AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd<*c8PpPQ;*RGOEU zTBKi|UzDvMoSIislv!GgU=&oAWaQ`RCZ+>r^uc7YetdjpUS>&ryk0@&FAkgB{FKt1 YRJ$TppqY$7Tnu7-WM*V!EMf+-0B0>J?EnA( literal 0 HcmV?d00001 diff --git a/python-agent/agents/__pycache__/base.cpython-312.pyc b/python-agent/agents/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ce232540d47afa6c27576d6cd2a6cd4d376f6ca GIT binary patch literal 3649 zcmaJ@U2Gf25#IYDdB-1#QY^`)Y~^GMa_LwS)TEC8phA{ZN0BWjmQmC!(8Gzhl#V`< z>fXsRS=lfQKR7_zBu`S17_pHD+i-#SQF%%L$3TJhL4+Mhxw?pfrVo9iA{8lIpy=!# zkECp-OK^8~_GWix_S>2Hb0iW((0XqrXWtGX^mjV(ny*4^UIk(a=}6~Hl;x6KmQV5= zo%5!U6_aAtm-J=*Nk8KWW*`}WHPMu^a#ChIpBc=Ck|7T92nRAdy#s0^j@;wiS+a>o zCy?&HhV%gD?gOP?SluG+SYhN;eB|UHr-K%Voly&JEN(OZqq=GnvMI27^?0ZoUT@#a$ z0-uOD@^Q_=OuypraK$iX8hA>|)GSL$=ZT`3rlL_3EM>v4XBE#1W%#Xe#mL!shG^94 z{z*s7X<6)qQp_}UPP1nnzdK2Z6~*Y_hd=f3<{|L@5?bUIkxKcjEeb?h6o6B?8H92N z7kScYd$>RuD*s$t zq52RRI!8{Ed{!-JS<{giiE|eSb%oPpRq&~@+jabAer5&}M>KNjyd$eBgMg|!lB&{s zRn>&(h!!@}?ims9AdB9u0`1w)2WInGJV54iIZOr?^5p!0hn5;KSAd6;>tlxwxWF2q zkn5j=2c)V7n53%3)+#dlJsvg6nuqQRTHmqry7;cV;YZ=t^_X&f^!F3rM0P%j^{vHT zFU4M82|bF9Qs#G*bFdUU_$YSh`slk8TYmKNfjgt0P28P$)OF~(_*m-vPHK8D^iJq~ z^X6-}hi(n6#gCNYM^^V9UHR#X{)O?mvGVKH9phh1Z*IcDC{+a|ok3$TruHwP0+-;6 z2TrlDpxAi@U&N_-8!PrKcHhrFOR?r~%19e2#n!Cz{gZc5f+HOuZP9%dLPL}jt_Z61 zn__#-5B2;mDinb33M%`N93e5Fw`oUjpK13O0Pzx%QDa#B3~|V=!aP%bWFTE>48B#U zdt8JlRBs41NH*R-L-lyB>F~T8K4O$%X<_^48H(uVnl66K|Iv4^qBlovYkuw%n0n8f zt-rX88tUe6teYOtrR`__f3G#(1$-vosD>&?QbGEw*-;3_(IB8<0TSIgxEXlp!HdE< zw7|uY9raA;L8Nert2_TCZ>2W===q^V4nj%Yz5N(68hR((cwf8CJ9&}w5MfhbvsU9Z zyvxYPLj7G{MRBnE9J-2J%k|(T?lQN)EmY=-refbI&B!SbW)wFI!yD6VWme1SCbrzi zrkF+=rwS<(_ZQ_UTO+oSn^B5>chw0EpBS4wt4^IAK64gQdyvH;7UrCgOS9-lm+%FM zld4^q!%jfQwq}@?)8vYeoIdr|$+5Fzj-1M8=S*y4J;4(NjCTB%t=aQdaZg2^>IaH7 zpGslNO3#~SVY1j{hSU;k8nvI<~bNWm|a5wW|Lh=Mup#xJT5hgUF5PI{3<`EW? zoepb$It%f0j^sgWEtkS7Q=icLA?>u=1Dzv}T})wCtB{}2xd1hdBT^ede|m2Yhb{WL zluA$%z}dw}Ef&Ev4$Ia_2wV_?#8>3j^_I4^mYz~e&j#8V8Rwov+m}KggqDpv;%D+* z`R4EL_pa_f{5X09;uU-ujhCYFwdm1O^yvD_2cGuszir$y*7v<~`|_>JYx{;u`-V1r z!k+fB549ZILjIQ4aw`+-E=9W^N8=4P8SpmQ+xML4Rur{#FlFpd3Lh4h&#xwavAX-j zgrg(k46zOF%zp^Nn-`9>fGncd@=(9*YYP_V=e3b6;!AE2-Y)R;CrD#6z8ckX(KO6%Bim--7N2Xq)A_?UhUl*}x z){9Ls*eRM)1D?Ww*FOn>Vo2Q&;(%ugu9`*`-5)9kH}kc;fZk$f4RS9mo&i~lrpzno zsT5w5I!jXLn)GT(diDOBUk1LG-e5^F!B0*mni%$-psHr``n(CXj1yMX-_C2MTiG}P zRn_w;RV4?gel)SN@`=!jnNU};cS8m!MRO#f36TuaZa3{{`ejBCT0XLl5*CK;8u~_@ z*btk16WsDt8PW01#TDW1C71rwuU3C)lul(f*-BYK{v8`U?~7~-K$R)=z0~JxE&qn& zedFAnkusv=O3#Y*<;cp#&-=I7+|_`dnTR@qHk~5WwM=!#4-XrnU!CIAKqo*8KHx*t zz~q7fL8*UxN$A#z&DB-6l&{p4GuS@K3Pv}qut3CY-|mlR(lOWNL*ytNO6wf!Fmz>s zr^uc7YesN(=JVYQqJ~J<~BtBlRpz;@o eO>TZlX-=wL5i8JiMj$Q*F+MUgGBOr116crT(k#%*A)Bt>eve{NB^OoF0bSr^oo2xe=@>{ zB)>NRQ~V(qwPcL`paTKWNAHBG9m6oumy(|Dws(OkYI+6r-pQs zXo%p*v$g>}N1a6jhDmgq7%=uDmq9kcQi{}=2Pj4%nTY5Q4Ko=^Jf|Wv?w3ZGe$Ykm zM?}USlK5f4EBV7Aw`}o+Lqq;ycFZe{_^|HkX8;1b=FurAKOm-2oDk}yH5Vdiam4kt z`ORoTC#|^=p;6UFRT9*3o$CAv5;hV3R9*swbbd5JN_aixJ7m8xaDs9paIQ zZ?nh_`2)Oc4u&~Cz=pg*KBgZYlj@sf`+%yXJfZ2TLlz?NIOa=l$0mrhniO`hwJ{T|0WO4Zv^}Z=t>YSpc%@@s2SBx<8iHHO2RDowJ`Q`SV zCeIG^)mFXd4dYik=v}Sk*QT0XjpWyjdZ2yjTnh{q_{XR6Q41X1X(R!e`bN`vSM7n! zNkD8~CkuZFIrU>wi+M0Bog*{2NsfY%bVk%hpjUx&S`Vh6Hyw+$5l;URb;F?M8UoK+ zCsT^`8J0lKfBu84yXFN&!g|`8IEte=GiUjT%v>*OU0*NZKaf+?*<}4WoK4jmkPY#Y zTu!JOPUtu*{!@90m+g{W!Vy9F-c9EU*rTk>J<{@jT*qbK$Wf(tpciftT0Yx%@a8r!*gA zV3#y9Q+>-7JiEK+-=$C}|0Q~qsN63+JEd@T%DD26beSjTB~lqpYSj8XW;p*OjTji! zNq`szCJY%;*E{DM*LKM8><+!abA^iMG^}S(b<%KLJl(e*qT`C5y`6wTtP_TJ41jqy zrzj1D0Q$+a<>*Uoam29|=@b1z@8g?WoU!X^jWPHCZOL=@UbQ3&HxXN;+2L}=YCNZT zUqs>=X@pnshLRO)MMfOsef}Z8kCD9M33trY2l*GjsbZwt?UwD3KiR__{oQQ;;f}t; zvQIm{3^C zKuH`^&jkIV@}5h>q!g5iq<~R|GM>J^1AVcg7s`@1bSiJVHBOI$OGp6&R-pz8&<%Gf zy7dY;hQ~IlhGU_Mb5d`|n1xA$%(fqJNCu#&7bUMC#ppw8@RNz@0O6TBLgF|tv@tS< z9qgeap1xyj|B=p4Pk+CQl(SShY$y^O#JElEkh1_yjsc1VT*rrorIA>%1|$I{`gl=H zYr)7_nsmhxE=p*{mc5P(Y@W~rMXX$tfze`GfXHA>2Kr{8)n_Nb*bYFes(LAg=kRLE zMFQ{TqJWJB*hvWhtWWZv;^9@!r(B?%oX!LL5B7Ktdt{>+@nKV3I%Owh19(x2h_MRI z<@h);ymC|lQ8K=8a4Z18lyl_@m0*)h`9nitnZf{H&JiPn0FYUXqXEf>e9xajp(5>& zJWS!2sepeU0e}l|n&x32FzDuG5>Tg1ijp8y<6#OPXbh|IIxOol3bmLOzv5&%?G>5y zbEg%j%O-XI1Q*Q7fVGT(AAGVkWfIR~`(pWEZwi!VcDG>8^N`ifp!+8K1BMYqBCmeYB}e_dqkdLg$ZrIEoYOof zEi+Zu>{sox(gM>owHJ_cPV=e(+WM%EOwySN| z+OM|Hy|Pfb?UHG^u;CpS^O zf3dLl87*7av~+ErJ#@Wb*}Zjc%k|+Ucl*4%9ky~!+sDXMRm|DcJKG`m&{9x1RiqwWAOkf35B) z*`1^Nn$SVxwqC#6ZoF+aVmgQ3)1<%c=*ZgBpugiXK-ZlHJ#^h^A~F3My|0k`26fo? z<&uBP)kDKY^tw#KZhTG5EDLY*NC57fY-QP#5pN)M17#!2a$z6K3LcoJJne*T%wb!5 zOZxag04^#aY=T}9N9kWEH&4L7S1AP!@*>VrKb=M2Q3oDS)%sr&S2}-0xc+Z1ag5Nn zJ%ZuXY*&^NEo38p?m{{g%3wq;4|qevO7en7b}$%$M|ac>Z3(N*2gKCt*`dwiPeWq# z2O$l518zv2sYi9GHbD~;k^BMp#ef%6FLVn3-YQ`1DAVvX?@?aeikFl?sF?IA5)NX8 zafo?hdL!k!cHt;Au?NI9C{{>=2.25.0 +PyJWT>=2.4.0 +cryptography>=3.4.0 +dataclasses-json>=0.5.0 \ No newline at end of file diff --git a/python-agent/services/__init__.py b/python-agent/services/__init__.py new file mode 100644 index 00000000..5abec40a --- /dev/null +++ b/python-agent/services/__init__.py @@ -0,0 +1 @@ +# Services module for Sentrius Python Agent \ No newline at end of file diff --git a/python-agent/services/__pycache__/__init__.cpython-312.pyc b/python-agent/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb3d44910deb8df63aa975e79b13ffc8ad69e055 GIT binary patch literal 166 zcmX@j%ge<81e+E_WoQHG#~= fu*uC&Da}c>D`Ewj$_T{8AjU^#Mn=XWW*`dy>{}}T literal 0 HcmV?d00001 diff --git a/python-agent/services/__pycache__/agent_client_service.cpython-312.pyc b/python-agent/services/__pycache__/agent_client_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f22146f1e2c9a7f630fed2ba1240c1b9c0a3ae3 GIT binary patch literal 8444 zcmbtZTWlN06&-S4Bt=maC0kEh>xW`diN!h%Qdy3j$g~qXb(F+%`XHJTYwk)WO_B2K z%8pnnb(<7WK8iXBP^V~%Iw(*maDb{`b^oO1$7p{jNPyUlfu;`>=tlu23XsC>NAI0| zh@@yMt_R?}?#$)PotZP|&i=8vIY1zMd?Rt;uR93&8y1`(*qM$0LgqSANrI>z)tmJs zJS_Dlye#Ds9838G4=I-wro@CeB_*UOU&80XK77_c6-Wf8ni5SO;w2iF>2&^NGu4`J zdaP0+qy@a>Iid=05>>qEwQ6tLO%pA)R064Qi!^LY{U8l&k+#~>CXfcVNF(fDn?V+G zWQn#}Z?vV54~$M~IYUlm)0pTQy_`;I@qc5G7Q~9Z>nOr)h80maYzLGXB$m7tC zre}24R-;3Klb2@Fmz6AbH}dlH%4J1nI^#^`xUDgjRdjtQ@K2oVSd=sQ6X}#;a-+Ff z)A!;us#mg6ujyA6Lt$-AQPEXsld{dj63#AcJOY{PL?f_dB;kDp79+uFye6pLr$EAT z9Oh>Es9<)o@toBr)4D;~RL*LbW;ER>SR<8bl?VzL(&5T(2l#I5`A)cLFGM0+tLGc<|@J*6qsIIk&2VFxSA7n};)?&4_t-082KG({b(G^3B1 z&9H%tq>?hym(#|qDNSj*4wH@sXb{KWj8O<9oG8U}NO88b6(bxO#dA-)F^XWsrrnOY zolb-q{ZWX}Mt$W-_tj_TaADT_&t^$ z?Ah3SCqPnfzD7Hh)$wfiijbEkyv&?}WzJbKDw!=W(fnmCr{q%FNl1;t&dmy0!y0GP zvO?0Boz_ghkp@#4%G9*!vo=XuH6>Lulyp`fi~1UxZ_{#k;wX<%ozZ%+4x`=h)AvGz zMw6|L)e?DQtQ=~6$QE?^lJleecRn?*r~Ct20?OfTd7t1C zTJFTT7YjWs$L24?A7SIq>!_cR7OFvPKfV6qmA8j`y?BLd*eBrPI_jrM@-S$PV5m=+?bn?8S zYY3|;+;&dDJpMMOA-YbAo{W8;It?&YJBg$eo_11JJ_^U({w(*}QVmfm+jNUmLn9jl))h`^=PdD}%S4u4N}! zSNa(A*Ks%Fm+-q<3y+WW*jzvk;I z%U{BD*PV{O^^OB89S2rBqVxO@gKY1?*fnE@=2EU*K#gnFXQ2RAB^gNTLk3&b1;=v4^kPb_1#aM{%eiS!Yo$ zgq7*(tW7jx89ko^GSEC)t*8);=W_;t+`)5zYf&$=q#2pijHJ67Oe=`K1Y0# z9aFa^u{n+}X%DNgzmd)BTRjOnY~z0%ZeI`guY~*8!^11#;nncSyzp_~BR6JlOqO;H zm3xOi5x8)mLO6fGI`~LL&0sZYsM+1P*Vyj<09NN->+n<9-#_=xaj;7FTM^!m>xgJw z7c%b7-xL(NBL9Kc;e&HRQOF=!s0+?&dkDxK$f|1N$j8Y(V&F~8LI!W*y0D+n^K+ts zR|^XnynyS1$`wW2QO;TR5CdVAg`%kP&S>dyQOw|-TNl3f*f#pb#{C-c_S$iLA6WaG z__}z-bA`OdeUn`AM5V%i18|60u*fA`ITq+Krfgi$HrX<=X{2Syn4L~;W|U6qBW?-B zObw%zBk~wf$k^!)hHznUxqBi5($ zIbCD7tJ|-zGX{_yyL`yvo-vC=Yc%RHCFgKifL6<|CxCcp>WGYxY3Uui8#(LzManwk zBXYFa!ppC~64Mw&CO@6m4KqxYG+YvJ!6YqeXz~ab6rs@!+r<9lYblL66{eWYPfo&R zFKCqJDU3|f6~mzP2-9e2uNiRlBV15Cr*s&rIk64AHJ9N~ZIff5LjN5^S4m|jiR@f&-MiAdw-nx2+IP4V8ky(HP!}0ki43ep2Ir-U z=<`R)9X;2JZx`1)239%-7TOjkOC1BN9mnQRm4of;!TyzC|61@sxvl5=(YKGj^K>b= z_uqF~I@VhTR$2x=CA>d!Cm8wBnYYfAdY)Xow3sS&jjRQa+-cc)?b5r0Yc2f?dwxCm z{$OeFnLFW~*Mn~dOMQnH_xyJ7H-o>8{U%mAbiA}Hz7{@F4z~WM;s<4SbtKjAJvF+U zEbnfMw{gpdMt8+S+=n3n3S+bnq!#Z+j-9GTAv7PuI4^+lHWi4-p)nu{cm*D z#gQWS0e90z&N-2xBrbPd$e)(N~LD#x6%%^liR20CM^=NZIQGAm{z{`x^Jl z$aRXKSUV0tPHE2fx~~d3{Yn7L=pqUDxN4OH2(Jha51na17VuwOmb ze`=)vg^~X8q5g?Pl(P;Rb8GRmz3;W~>S@RqBAb8bU@Zf(F^b-0w6zLX@d`D0^bk!S z(Ax>jF|egaK|=A|QUorFiW$VxXD~O4(Q%C87$Hp36A(rH7M9jx0X>Ow1XMHRPA!S= zD^^crpgM978)6-{7Sds3AW^f8(FkN~n|vKL{R1KZ+3s499Vvy5)PwBby8u1A?sV_| z#lfE(EDb!p+Wkzqz2`HD?+(m|D#2hK0)uZ`?(Dm>qvzeR)g6ykB+?n(Abe+N{!}GE zns+=9A|G5(7RF0mht`6RKLjG<+An)Y#p8bRdw*N}829^Lz~sZ+hld3yz@>+e0x8!4 zg4Qk+FKtgZ!Fjqb-Q*sKZhDKH;S$WE>#hL>b%2>K^7o^d0Hk~)h^_%rmw0Aeq6xFM z0|QdQz?TdbGA_AvYQT}?ib5@b86A$a2;O)V1r-|6MgY?L>lcAYyHM+BCM1JvSr=-@ z!6>Nkx=;nAQsK6XJmWLxr_zQz?YlPY9AR8U?i|<7z z`)V<8a~?6B&5WJ~>lAiZ4TRNOIvW99s0!$iK>oj&CR&&_hALbnt3oaj1Qv2x+_2Re zZ*p@64V}y_t-b?#^xF^tc(vDp*YQ#)UJqUs{@(a>@|{;74xSF*+H|o zDZ)#|&%ATO1Hl$}$wi@>J%R`6!WhiY4h{$bNQw6Yu`lQ+OWXLdd{L?ehbli=QiUpz zk{U6}ZT)2Av<>|0M!;1&4ge`}PI_Ic0x4hNUoJMpDZKfE4-3Aqb#TEwli`k{vtP(9 zc6Q8&{EEGITo2b)=>8L)$jxmvBAum6Kau(ri7=HRvJ!C0ElLNpu>J&VyM zti6Io#^)F-!*@~(+gwt{X0O4+CtJXVo^NfFAA=744-f&ab=KnAa4B?n3$BTzYfrgr zAH1VQ0`p=eQ0)lg)8+!V`1Pf)mGF)8%!9kaPb|K?7%z1VuLTc31hU};{;Q*-`^obD zws;q}{1pJUcJ9M=0Sd6J$QZ_=p?iN*z}H-gyoa*)4IXpPVe|qBnKvD^aLg2;|3*5r^g73X!8w84#aEB0CXsZxNZpB%$ zbmDf~?bPkg<*^N{tvpVS#69Ky=qKD>A#y{n5J(oGDJ12-y%nr24?Vv4x}fcMD+H2TPb_gu<4eBZ!9PN=@?@{229jG`g+Q{zFOA>cxAcwW&}Ue*UPy5+ z(RS;@o$F|r629D|bCdLW5St==%)$b*88a6?1DH+j_hL<_co8$7#q`6GIKv)(tR-On z3Zl04t&iDvFBX|CYrkyW^*v>M#63p81x?`QA^I;Ns&F2U=MyjI;WvE56a0v@d_+PY zk)}VBM^?xqe;CQ)fA_n?tN#60#fnJ0O`k)TUU(1o^loratgvE`33$)m o3W3NLU@^o50``E&7GN=KtLj5leXgp$&x0qtp2(*JV`k3(0g;zg+W-In literal 0 HcmV?d00001 diff --git a/python-agent/services/__pycache__/config.cpython-312.pyc b/python-agent/services/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b590a4e6cc0a9f8e7e198dfc887b7faa00470494 GIT binary patch literal 6868 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 GS^p0v*vOXv literal 0 HcmV?d00001 diff --git a/python-agent/services/__pycache__/keycloak_service.cpython-312.pyc b/python-agent/services/__pycache__/keycloak_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc8ee6434c5f2507ef670c701eea6d69d79fc71f GIT binary patch literal 7336 zcmd^DU2GfImA=E7;h#nlCCVl#Thb_!Y|*jE*m505mgVZlj-$A4Hcqp(x?N(#8A+u1 zqud$VCPAfv(FYX>Pz$?&9AvxJBZUMg)0e7Gjh)ARAwv~nCeGHt?n}`ZB?_>G`?BZW z;gA%?*ulQ;g>-+<+_^k+zVn@P|D(0liJ<&$V{YkI7efC<24>+ZoxS&=vyNmmhh#>! z#F#mTww5^yZP_^%S~kYT`8hsrowLSmb2f&^axr_{G3TItKIV+K%(c+IB__-X7NoGz zZu4ceR%!c)F-CLk7W68Tt-nOF?ITNN&XuFi`8?Pv5>I7CE>MKbhtFu`I~3L7UjYI|j{mxIna zQV=K+5e0(sED9&H@Xx_NuW$=?_{rQHFWaDRk?qj8$_`kwDYgYpcEZ?R8N;j(_Ji3L zm~|eQ6=1gIz-%kb3UV9Wy&d|kvP*%H(!OA!yIJH8n02K){cgS8)LKO&nO98J5Ph4A zjrxYq04lwyB*eIskQS9V=s2J|K})w397^Gs&SOQ2#dSwG7J;=;MAlm?T~!HVMGNy4 zg^`A>{O>J6XB}mkXvIRvqFH3dw3{pnRoAl?7*%Wsp)3m{vKuW~4o0SxqX>dg^{r{- zw_JP51pQXsid79qmN7vJZXuOGs#d7H?jDP|YT97CY>|71PyQt-dDl>uQ zv~CZDB8i9=3T3?JH>rH5fVtv^cdMtNdJmPm2Y0$(EOx(`n=W;~w938TUOv+X3>wRa;0s`- z`W0&HuwwZQw*uY+)~jElwhpt_+Q{t5+`q9a76;NC=1i1&F>_?JSte`wm^b~#3Y%r4 zoixQhs<8Bm2Fx6 zL-ZSqX|oKvi)8x>|1N)cU!(LR9ap0I%+G@N~{aECDdJ z^s-X3ckpbSENPN17-tI69WuR_rASO6kQW!>yru;jCms{8`CFQyLAPV&os^<#s?IGZ zRZVwcDWWPNa4aEJle84fN7ZCPcfD!IUAhxisQcBevE<^Sf^}ZOIEnpMOmwX~=xK<1 z!AHm#_E(pa302XZ#tJ<;qo~BDC=5kRP@#9isR@J)SQ)?R1@l*!I65dp$V>fqsNO?Q zJV(}lvG$9?;o}>3^4#y8pEz^b!ucD8uDMmmt`!{#mVG@BdOzqb_w?`d1dBbvuWg*m zxytPdHb>`UPv82=+RDa_{9D_evDGVOp=(DNDhfl}!tt`N|3S|OJsfenPtUtj&z)xwciw)(f+1>c2jVWKQ_{C$^)#iyzVj<|Jx z@(g--#yvH}KC=0yMD~%$Ltp3VuMO8@vx?~nifIQybv@=`H&U`yf4gGITGYc@b&qJ( ziyD%ou6bt$rNJjhZM8MCm#w0KpjGufXGb<`*^i*K<+&b#mAOR*yh4Qo;4D@^Gx6uN z0CHbg;hyteTbA4Z-WvqD+`C*F$eOhK5bS>&;sP){MU=$Ze|$?c5NjHjXz+&!#uvyO zy_OV@-;Bt|A(lx2gGLhci6upn6?{3<2g^0_iVR_4`plT9^KV{1{e!cBQKuAJOk#+6 z;_6!&4|ywuIph!|7FwWKC8)INLP)uzVJQsOACd73paVPrmEImA)?VW_osHhsbgQg{ zld_^aszCJf6nWTB9%5k+-C27GABWi*DDko7wuw&+^sRt8e^{XS~@qdNtJ+#Yp zI($z$5ASpi6gvktE|oe@tWJO7KK$5qcwJZ%3O(nyUE^iXQGi-^?~ZSz=o`s}OTN>q zvtPJ-OrgGsEpN zsr*2}cXnHN`LWweLFId@5@p>To;-yG142 z0s^bTsu?+EX_y^hK4z;V20&{HF3zNpJB-=^RMuRIH>D_rRf&}vJzGaFdTmA=rjd*op)94P{URkZ$*@jh>jB*b0b*n! z9a1BU37`P1T;61KTavncRcEElGG(L8(HiAcOjtBFe{BX#d((=8@C)!uQ0Xk#GCoCy zRRiDT>g?8Ln6+%!D~Un~HyGyXGVwt-hcGo$WjE7g8zrV;166{Gm@V({K0fSQkFG^G z4i!#KfBKWcORsMqzPkEq+198}vTiBXPyu@r&q>j>!}3L%Vl!hpvrBnqcq&Osn zBw8X4kkVWLp9PIw#xFyycP?XP0pbTm4w>RQtET3gnU*O4riI7gVLT3%`W>cX3V*k$ zm=agXF7r@*A5$sxjh5VJK6lS<+5Xt_hn7z-mCnr;gxT+DEBGS3rny;s3@hKq8=2mN z(0x-cR1FCJ5B|l{#J{lAzpyfG>--CsdC~mP#J?DnISf=HzBW_wDddwIycxhG=gD{u z`U{{d!h(jY&<_#DOC_lLiGWQQPvi6OaD(&k*E&iaB-9^4 zWq$ud-kn2%;-Nq;RXX&8)yrkKZ+&cS?7mWR4;F;MgP(dIHJNTfg^L_?cW4;p9FuPL z6Ypdj`>>6NF4aVH8~9He>OsoXG=aN?aC`HWEMufZG)T0-dmBn6^&^UDD3#RDqE!RI zXf>&-iS7>xqfMB}Oweco_lyLOByiDc!dQEQ%q}96UuV$I0o^NnmTxLqW!Wrqa~QrH z^S&3f(j;ljR+)CinzeqH=7KPZhL~Q z7Tf_F)+K*EWJod9M|vVudMAW}HzAX(i#Hn)XE*4YAX&GAv`Uk_Au{C1P`AU8LL|Mz zqcCYOIWZ@~Yv(zm{PL4tL!CqT8Pv zD7pg$A+XE292cH+_dW=I5G?pkosbv-b2qxbnf3jkj}emX5yk4dPpm-LscLVtj3UgD<&{g6z`ZdD)B%vU^je>?GJ&)A>0Ky01mvz+NM2)?7l#v2OAv8HD-PfA&_ zdMQNl&1=M0(-I|FdP9*Njh94_YC6d!QQB#=b@*<{j7}#Be`yf#ye<+jkPDeGPA_Xo zxXf8zO4p%NOwLE)3Mhby(}tAM_2%`-H|SmFwN!jw!79wP%gTb3ifN&FDGYbHvd$zj zmJ1hjc0RHQ%Z~Z9rX(@srW9|Pp&G@)1yl7PaE6-CAx=TiN9qq1SdubvNee9FNIar} zD^dfJnvTaIXpe*gaF33E3`f+DHhZRWFcrT+MA%3|)14&b3z4I#x^-Ssm9uB`4!9Ch zr3EE4fA)+K>>F-ke^TJL8D54EGJPlA75@Vn5_;F$D!FaK(2z=)i+UH+zq|$$nFAL` z2i@CkkW+sL72*93hxaSt?XQHH|8}|8+t%73UEOgF7hS_SZ+>92XRE8|zff{bl)L+O zdDPLjhuDs`)yZ$|sH5xqi+}Azy@8yR?$;mKGvv9MH2?Q|kDfsgFWHN@&kTLNlc!E?UpQ5o8b>EnT z5|~^tVWRC#gh>))xOc+tm{f#BRRX_XqA$2<$in!+jnQ2MO-`lFmeCZhzr9ChciTGn z&W-6k1kLWTF5b5>u#2F{4epU<_oR*YZiIIcG`Xd`x*6Xi-QCV-Ov465dN@D2IZ~1K z@Le0DdkC7{({5NhlV|hS^Y+b&ZxIZQ-$73A@4$qEj0~0#rfwtGop8~sTj4quT0=Wk zP3XPNuu;PkChl4HKU=5{beI@6{waCL0a5W)de@47MkZ+nP4!GN+?wv6+Q*G=J%JZt z0ayU3Q&8=)48weFVHs}Ej+mA&QTvyu?a$~~5gq#qo%;&C@+E40;^^QoMj_&*C VN{+$ztp8)pFbv$nkea^Ze*sfr%EbTx literal 0 HcmV?d00001 diff --git a/python-agent/services/__pycache__/sentrius_agent.cpython-312.pyc b/python-agent/services/__pycache__/sentrius_agent.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..016f6ee34c92a8713d3c2f632cf883d52ea355ef GIT binary patch literal 8654 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 literal 0 HcmV?d00001 diff --git a/python-agent/services/agent_client_service.py b/python-agent/services/agent_client_service.py new file mode 100644 index 00000000..f7577ead --- /dev/null +++ b/python-agent/services/agent_client_service.py @@ -0,0 +1,178 @@ +""" +Agent client service for API communication with Sentrius server. +Equivalent to Java AgentClientService class. +""" +import json +import requests +import logging +import time +from typing import Dict, Any, Optional +from dataclasses import dataclass, asdict + +logger = logging.getLogger(__name__) + + +@dataclass +class AgentRegistrationRequest: + """Agent registration request data.""" + agent_name: str + agent_callback_url: str + + +@dataclass +class AgentHeartbeat: + """Agent heartbeat data.""" + status: str = "ACTIVE" + last_activity: Optional[str] = None + message: Optional[str] = None + + +@dataclass +class ProvenanceEvent: + """Provenance event data.""" + event_type: str + timestamp: str + agent_id: str + details: Dict[str, Any] + + +@dataclass +class TokenDTO: + """Token data transfer object.""" + access_token: str + token_type: str = "Bearer" + + +class AgentClientService: + """Service for agent API communication with Sentrius server.""" + + def __init__(self, api_base_url: str, keycloak_service): + self.api_base_url = api_base_url.rstrip('/') + self.keycloak_service = keycloak_service + self.session = requests.Session() + + def _get_auth_headers(self) -> Dict[str, str]: + """Get authorization headers with Keycloak token.""" + token = self.keycloak_service.get_keycloak_token() + return { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + + def register_agent(self, agent_name: str, callback_url: str) -> Dict[str, Any]: + """ + Register agent with the Sentrius API server. + + Args: + agent_name: Name/ID of the agent + callback_url: Callback URL for the agent + + Returns: + Registration response data + """ + url = f"{self.api_base_url}/api/v1/agent/register" + headers = self._get_auth_headers() + + # Registration request doesn't need a body based on Java implementation + try: + response = self.session.post(url, headers=headers) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + logger.error(f"Agent registration failed: {e}") + if hasattr(e, 'response') and e.response is not None: + logger.error(f"Response content: {e.response.text}") + raise + + def send_heartbeat(self, agent_id: str, status: str = "ACTIVE", + message: str = None) -> Dict[str, Any]: + """ + Send heartbeat to the Sentrius API server. + + Args: + agent_id: Agent identifier + status: Agent status + message: Optional status message + + Returns: + Heartbeat response data + """ + url = f"{self.api_base_url}/api/v1/agent/heartbeat" + headers = self._get_auth_headers() + + heartbeat_data = AgentHeartbeat( + status=status, + last_activity=time.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + message=message + ) + + try: + response = self.session.post( + url, + headers=headers, + json=asdict(heartbeat_data) + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + logger.error(f"Heartbeat failed: {e}") + if hasattr(e, 'response') and e.response is not None: + logger.error(f"Response content: {e.response.text}") + raise + + def submit_provenance(self, event: ProvenanceEvent) -> Dict[str, Any]: + """ + Submit provenance event to the Sentrius API server. + + Args: + event: Provenance event data + + Returns: + Submission response data + """ + url = f"{self.api_base_url}/api/v1/agent/provenance/submit" + headers = self._get_auth_headers() + + try: + response = self.session.post( + url, + headers=headers, + json=asdict(event) + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + logger.error(f"Provenance submission failed: {e}") + if hasattr(e, 'response') and e.response is not None: + logger.error(f"Response content: {e.response.text}") + raise + + def create_session(self, username: str, ip_address: str) -> Dict[str, Any]: + """ + Create a session log entry. + + Args: + username: Username for the session + ip_address: IP address of the client + + Returns: + Session creation response data + """ + # This appears to be a GET endpoint based on Java implementation + url = f"{self.api_base_url}/api/v1/agent/session" + headers = self._get_auth_headers() + + params = { + 'username': username, + 'ipAddress': ip_address + } + + try: + response = self.session.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + logger.error(f"Session creation failed: {e}") + if hasattr(e, 'response') and e.response is not None: + logger.error(f"Response content: {e.response.text}") + raise \ No newline at end of file diff --git a/python-agent/services/config.py b/python-agent/services/config.py new file mode 100644 index 00000000..5027e637 --- /dev/null +++ b/python-agent/services/config.py @@ -0,0 +1,133 @@ +""" +Configuration management for Sentrius Python Agent. +""" +import yaml +import os +import logging +from dataclasses import dataclass +from typing import Optional, Dict, Any + +logger = logging.getLogger(__name__) + + +@dataclass +class KeycloakConfig: + """Keycloak configuration.""" + server_url: str + realm: str + client_id: str + client_secret: str + + +@dataclass +class AgentConfig: + """Agent configuration.""" + name_prefix: str + agent_type: str = "python" + callback_url: str = "http://localhost:8080" + api_url: str = "http://localhost:8080" + heartbeat_interval: int = 30 # seconds + + +@dataclass +class LLMConfig: + """LLM configuration.""" + enabled: bool = False + provider: str = "openai" + model: str = "gpt-3.5-turbo" + api_key: Optional[str] = None + endpoint: Optional[str] = None + + +@dataclass +class SentriusAgentConfig: + """Main configuration class for Sentrius Python Agent.""" + keycloak: KeycloakConfig + agent: AgentConfig + llm: LLMConfig + + @classmethod + def from_yaml(cls, config_path: str) -> 'SentriusAgentConfig': + """Load configuration from YAML file.""" + try: + with open(config_path, 'r') as f: + config_data = yaml.safe_load(f) + + return cls( + keycloak=KeycloakConfig(**config_data.get('keycloak', {})), + agent=AgentConfig(**config_data.get('agent', {})), + llm=LLMConfig(**config_data.get('llm', {})) + ) + except Exception as e: + logger.error(f"Failed to load configuration from {config_path}: {e}") + raise + + @classmethod + def from_env(cls) -> 'SentriusAgentConfig': + """Load configuration from environment variables.""" + try: + keycloak_config = KeycloakConfig( + server_url=os.getenv('KEYCLOAK_SERVER_URL', 'http://localhost:8080'), + realm=os.getenv('KEYCLOAK_REALM', 'sentrius'), + client_id=os.getenv('KEYCLOAK_CLIENT_ID', ''), + client_secret=os.getenv('KEYCLOAK_CLIENT_SECRET', '') + ) + + agent_config = AgentConfig( + name_prefix=os.getenv('AGENT_NAME_PREFIX', 'python-agent'), + agent_type=os.getenv('AGENT_TYPE', 'python'), + callback_url=os.getenv('AGENT_CALLBACK_URL', 'http://localhost:8080'), + api_url=os.getenv('AGENT_API_URL', 'http://localhost:8080'), + heartbeat_interval=int(os.getenv('AGENT_HEARTBEAT_INTERVAL', '30')) + ) + + llm_config = LLMConfig( + enabled=os.getenv('LLM_ENABLED', 'false').lower() == 'true', + provider=os.getenv('LLM_PROVIDER', 'openai'), + model=os.getenv('LLM_MODEL', 'gpt-3.5-turbo'), + api_key=os.getenv('LLM_API_KEY'), + endpoint=os.getenv('LLM_ENDPOINT') + ) + + return cls( + keycloak=keycloak_config, + agent=agent_config, + llm=llm_config + ) + except Exception as e: + logger.error(f"Failed to load configuration from environment: {e}") + raise + + def to_dict(self) -> Dict[str, Any]: + """Convert configuration to dictionary.""" + return { + 'keycloak': { + 'server_url': self.keycloak.server_url, + 'realm': self.keycloak.realm, + 'client_id': self.keycloak.client_id, + 'client_secret': self.keycloak.client_secret + }, + 'agent': { + 'name_prefix': self.agent.name_prefix, + 'agent_type': self.agent.agent_type, + 'callback_url': self.agent.callback_url, + 'api_url': self.agent.api_url, + 'heartbeat_interval': self.agent.heartbeat_interval + }, + 'llm': { + 'enabled': self.llm.enabled, + 'provider': self.llm.provider, + 'model': self.llm.model, + 'api_key': self.llm.api_key, + 'endpoint': self.llm.endpoint + } + } + + def save_to_yaml(self, config_path: str): + """Save configuration to YAML file.""" + try: + with open(config_path, 'w') as f: + yaml.dump(self.to_dict(), f, default_flow_style=False) + except Exception as e: + logger.error(f"Failed to save configuration to {config_path}: {e}") + raise \ No newline at end of file diff --git a/python-agent/services/key_service.py b/python-agent/services/key_service.py new file mode 100644 index 00000000..2b7c443f --- /dev/null +++ b/python-agent/services/key_service.py @@ -0,0 +1,107 @@ +""" +RSA key generation utilities for secure communication. +Equivalent to Java EphemeralKeyGen class. +""" +import base64 +import logging +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.backends import default_backend +from typing import Tuple + +logger = logging.getLogger(__name__) + + +class EphemeralKeyGen: + """Utility class for RSA key generation and cryptographic operations.""" + + @staticmethod + def generate_ephemeral_rsa_keypair(key_size: int = 2048) -> Tuple: + """ + Generate an ephemeral RSA key pair. + + Args: + key_size: Size of the RSA key (default 2048) + + Returns: + Tuple of (private_key, public_key) + """ + try: + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=key_size, + backend=default_backend() + ) + public_key = private_key.public_key() + return private_key, public_key + except Exception as e: + logger.error(f"Failed to generate RSA key pair: {e}") + raise + + @staticmethod + def get_base64_public_key(public_key) -> str: + """ + Convert public key to base64 encoded string. + + Args: + public_key: RSA public key object + + Returns: + Base64 encoded public key string + """ + try: + pem = public_key.public_key_pem() if hasattr(public_key, 'public_key_pem') else \ + public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + return base64.b64encode(pem).decode('utf-8') + except Exception as e: + logger.error(f"Failed to encode public key to base64: {e}") + raise + + @staticmethod + def decrypt_rsa_with_private_key(encrypted_secret: str, private_key) -> str: + """ + Decrypt RSA encrypted data using private key. + + Args: + encrypted_secret: Base64 encoded encrypted data + private_key: RSA private key object + + Returns: + Decrypted string + """ + try: + encrypted_data = base64.b64decode(encrypted_secret) + decrypted_bytes = private_key.decrypt( + encrypted_data, + padding.PKCS1v15() + ) + return decrypted_bytes.decode('utf-8') + except Exception as e: + logger.error(f"Failed to decrypt RSA data: {e}") + raise + + @staticmethod + def encrypt_rsa_with_public_key(data: str, public_key) -> str: + """ + Encrypt data using RSA public key. + + Args: + data: String data to encrypt + public_key: RSA public key object + + Returns: + Base64 encoded encrypted data + """ + try: + data_bytes = data.encode('utf-8') + encrypted_bytes = public_key.encrypt( + data_bytes, + padding.PKCS1v15() + ) + return base64.b64encode(encrypted_bytes).decode('utf-8') + except Exception as e: + logger.error(f"Failed to encrypt RSA data: {e}") + raise \ No newline at end of file diff --git a/python-agent/services/keycloak_service.py b/python-agent/services/keycloak_service.py new file mode 100644 index 00000000..ebb2913f --- /dev/null +++ b/python-agent/services/keycloak_service.py @@ -0,0 +1,138 @@ +""" +Keycloak service for handling authentication with Keycloak server. +Equivalent to Java KeycloakService class. +""" +import jwt +import requests +import logging +from typing import Optional, Dict, Any +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +import base64 + +logger = logging.getLogger(__name__) + + +class KeycloakService: + """Service for Keycloak authentication and token management.""" + + def __init__(self, server_url: str, realm: str, client_id: str, client_secret: str): + self.server_url = server_url.rstrip('/') + self.realm = realm + self.client_id = client_id + self.client_secret = client_secret + self.public_keys_cache = {} + + def get_keycloak_token(self) -> str: + """Get access token from Keycloak using client credentials.""" + token_url = f"{self.server_url}/realms/{self.realm}/protocol/openid-connect/token" + + data = { + 'grant_type': 'client_credentials', + 'client_id': self.client_id, + 'client_secret': self.client_secret + } + + try: + response = requests.post(token_url, data=data) + response.raise_for_status() + token_data = response.json() + return token_data['access_token'] + except requests.RequestException as e: + logger.error(f"Failed to get Keycloak token: {e}") + raise + + def validate_jwt(self, token: str) -> bool: + """Validate a JWT using Keycloak public key.""" + try: + # Extract kid from JWT header + kid = self._extract_kid(token) + if not kid: + logger.error("No 'kid' found in JWT header") + return False + + # Get public key for kid + public_key = self._get_public_key(kid) + if not public_key: + logger.error(f"No public key found for 'kid': {kid}") + return False + + # Validate JWT + jwt.decode(token, public_key, algorithms=['RS256']) + return True + except Exception as e: + logger.error(f"JWT validation failed: {e}") + return False + + def extract_agent_id(self, token: str) -> Optional[str]: + """Extract the client ID (agent identity) from a valid JWT.""" + try: + decoded = jwt.decode(token, options={"verify_signature": False}) + return decoded.get('azp') or decoded.get('client_id') + except Exception as e: + logger.error(f"Failed to extract agent ID: {e}") + return None + + def extract_username(self, token: str) -> Optional[str]: + """Extract username from JWT token.""" + try: + decoded = jwt.decode(token, options={"verify_signature": False}) + return decoded.get('preferred_username') or decoded.get('sub') + except Exception as e: + logger.error(f"Failed to extract username: {e}") + return None + + def _extract_kid(self, token: str) -> Optional[str]: + """Extract the 'kid' (Key ID) from JWT header.""" + try: + header = jwt.get_unverified_header(token) + return header.get('kid') + except Exception as e: + logger.error(f"Failed to extract kid: {e}") + return None + + def _get_public_key(self, kid: str): + """Get public key for the given kid.""" + if kid in self.public_keys_cache: + return self.public_keys_cache[kid] + + try: + # Fetch JWKS from Keycloak + jwks_url = f"{self.server_url}/realms/{self.realm}/protocol/openid-connect/certs" + response = requests.get(jwks_url) + response.raise_for_status() + jwks = response.json() + + # Find the key with matching kid + for key_data in jwks.get('keys', []): + if key_data.get('kid') == kid: + # Convert JWK to public key object + public_key = self._jwk_to_public_key(key_data) + self.public_keys_cache[kid] = public_key + return public_key + + except Exception as e: + logger.error(f"Failed to fetch public key: {e}") + + return None + + def _jwk_to_public_key(self, jwk_data: Dict[str, Any]): + """Convert JWK data to cryptography public key object.""" + try: + from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers + from cryptography.hazmat.backends import default_backend + + n = int.from_bytes( + base64.urlsafe_b64decode(jwk_data['n'] + '=='), + byteorder='big' + ) + e = int.from_bytes( + base64.urlsafe_b64decode(jwk_data['e'] + '=='), + byteorder='big' + ) + + public_numbers = RSAPublicNumbers(e, n) + return public_numbers.public_key(default_backend()) + except Exception as e: + logger.error(f"Failed to convert JWK to public key: {e}") + return None \ No newline at end of file diff --git a/python-agent/services/sentrius_agent.py b/python-agent/services/sentrius_agent.py new file mode 100644 index 00000000..617515d0 --- /dev/null +++ b/python-agent/services/sentrius_agent.py @@ -0,0 +1,163 @@ +""" +Main Sentrius Agent framework that integrates all services. +Equivalent to Java ChatAgent functionality. +""" +import time +import threading +import logging +import uuid +from typing import Optional, Dict, Any +from datetime import datetime, timezone + +from .keycloak_service import KeycloakService +from .agent_client_service import AgentClientService, ProvenanceEvent +from .key_service import EphemeralKeyGen +from .config import SentriusAgentConfig + +logger = logging.getLogger(__name__) + + +class SentriusAgent: + """Main Sentrius Agent framework class.""" + + def __init__(self, config: SentriusAgentConfig): + self.config = config + self.agent_id = f"{config.agent.name_prefix}-{uuid.uuid4().hex[:8]}" + self.running = False + self.heartbeat_thread: Optional[threading.Thread] = None + + # Initialize services + self.keycloak_service = KeycloakService( + server_url=config.keycloak.server_url, + realm=config.keycloak.realm, + client_id=config.keycloak.client_id, + client_secret=config.keycloak.client_secret + ) + + self.agent_client_service = AgentClientService( + api_base_url=config.agent.api_url, + keycloak_service=self.keycloak_service + ) + + # Generate ephemeral keys for secure communication + self.private_key, self.public_key = EphemeralKeyGen.generate_ephemeral_rsa_keypair() + + logger.info(f"Initialized Sentrius Agent: {self.agent_id}") + + def start(self): + """Start the agent and begin registration process.""" + logger.info(f"Starting Sentrius Agent: {self.agent_id}") + + try: + # Register with the API server + self._register_agent() + + # Start heartbeat mechanism + self._start_heartbeat() + + self.running = True + logger.info(f"Sentrius Agent {self.agent_id} started successfully") + + except Exception as e: + logger.error(f"Failed to start agent: {e}") + self.stop() + raise + + def stop(self): + """Stop the agent and cleanup resources.""" + logger.info(f"Stopping Sentrius Agent: {self.agent_id}") + + self.running = False + + # Stop heartbeat thread + if self.heartbeat_thread and self.heartbeat_thread.is_alive(): + self.heartbeat_thread.join(timeout=5) + + logger.info(f"Sentrius Agent {self.agent_id} stopped") + + def submit_provenance_event(self, event_type: str, details: Dict[str, Any]): + """Submit a provenance event to the API server.""" + try: + event = ProvenanceEvent( + event_type=event_type, + timestamp=datetime.now(timezone.utc).isoformat(), + agent_id=self.agent_id, + details=details + ) + + response = self.agent_client_service.submit_provenance(event) + logger.debug(f"Provenance event submitted: {response}") + + except Exception as e: + logger.error(f"Failed to submit provenance event: {e}") + raise + + def get_agent_id(self) -> str: + """Get the agent ID.""" + return self.agent_id + + def get_public_key_base64(self) -> str: + """Get the base64 encoded public key.""" + return EphemeralKeyGen.get_base64_public_key(self.public_key) + + def decrypt_with_private_key(self, encrypted_data: str) -> str: + """Decrypt data using the agent's private key.""" + return EphemeralKeyGen.decrypt_rsa_with_private_key(encrypted_data, self.private_key) + + def _register_agent(self): + """Register the agent with the API server.""" + try: + response = self.agent_client_service.register_agent( + agent_name=self.agent_id, + callback_url=self.config.agent.callback_url + ) + logger.info(f"Agent registration successful: {response}") + + # Submit registration provenance event + self.submit_provenance_event( + event_type="AGENT_REGISTRATION", + details={ + "agent_id": self.agent_id, + "callback_url": self.config.agent.callback_url, + "agent_type": self.config.agent.agent_type + } + ) + + except Exception as e: + logger.error(f"Agent registration failed: {e}") + raise + + def _start_heartbeat(self): + """Start the heartbeat mechanism.""" + if self.heartbeat_thread and self.heartbeat_thread.is_alive(): + return + + self.heartbeat_thread = threading.Thread(target=self._heartbeat_worker, daemon=True) + self.heartbeat_thread.start() + logger.info("Heartbeat mechanism started") + + def _heartbeat_worker(self): + """Heartbeat worker thread.""" + while self.running: + try: + response = self.agent_client_service.send_heartbeat( + agent_id=self.agent_id, + status="ACTIVE", + message="Agent running normally" + ) + logger.debug(f"Heartbeat sent: {response}") + + except Exception as e: + logger.error(f"Heartbeat failed: {e}") + + # Wait for next heartbeat interval + time.sleep(self.config.agent.heartbeat_interval) + + def __enter__(self): + """Context manager entry.""" + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + self.stop() \ No newline at end of file diff --git a/python-agent/test.db b/python-agent/test.db new file mode 100644 index 00000000..e69de29b diff --git a/python-agent/tests/__init__.py b/python-agent/tests/__init__.py new file mode 100644 index 00000000..18dae6c2 --- /dev/null +++ b/python-agent/tests/__init__.py @@ -0,0 +1 @@ +# Tests module for Sentrius Python Agent \ No newline at end of file diff --git a/python-agent/tests/test_integration.py b/python-agent/tests/test_integration.py new file mode 100644 index 00000000..fa3d437a --- /dev/null +++ b/python-agent/tests/test_integration.py @@ -0,0 +1,182 @@ +""" +Integration test with mocked services to validate agent functionality. +""" +import unittest +import os +import sys +from unittest.mock import Mock, patch, MagicMock +import tempfile +import yaml + +# Add parent directory to Python path for imports +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from services.config import SentriusAgentConfig, KeycloakConfig, AgentConfig, LLMConfig +from services.sentrius_agent import SentriusAgent +from agents.sql_agent.sql_agent import SQLAgent + + +class TestSentriusAgentIntegration(unittest.TestCase): + """Test the complete agent integration with mocked services.""" + + def setUp(self): + """Set up test environment.""" + self.config = SentriusAgentConfig( + keycloak=KeycloakConfig( + server_url='http://localhost:8080', + realm='test-realm', + client_id='test-client', + client_secret='test-secret' + ), + agent=AgentConfig(name_prefix='test-agent'), + llm=LLMConfig() + ) + + @patch('services.sentrius_agent.KeycloakService') + @patch('services.sentrius_agent.AgentClientService') + def test_sentrius_agent_initialization(self, mock_agent_client, mock_keycloak): + """Test SentriusAgent initialization.""" + # Mock the services + mock_keycloak_instance = Mock() + mock_keycloak.return_value = mock_keycloak_instance + + mock_agent_client_instance = Mock() + mock_agent_client.return_value = mock_agent_client_instance + + # Create agent + agent = SentriusAgent(self.config) + + # Verify initialization + self.assertTrue(agent.agent_id.startswith('test-agent')) + self.assertFalse(agent.running) + + # Verify services were created + mock_keycloak.assert_called_once() + mock_agent_client.assert_called_once() + + @patch('services.sentrius_agent.KeycloakService') + @patch('services.sentrius_agent.AgentClientService') + def test_sentrius_agent_start_stop(self, mock_agent_client, mock_keycloak): + """Test agent start and stop functionality.""" + # Mock the services + mock_keycloak_instance = Mock() + mock_keycloak.return_value = mock_keycloak_instance + + mock_agent_client_instance = Mock() + mock_agent_client_instance.register_agent.return_value = {'status': 'success'} + mock_agent_client_instance.submit_provenance.return_value = {'status': 'success'} + mock_agent_client.return_value = mock_agent_client_instance + + # Create and start agent + agent = SentriusAgent(self.config) + agent.start() + + # Verify agent is running + self.assertTrue(agent.running) + + # Verify registration was called + mock_agent_client_instance.register_agent.assert_called_once() + + # Stop agent + agent.stop() + + # Verify agent is stopped + self.assertFalse(agent.running) + + def test_sql_agent_initialization_with_config(self): + """Test SQL agent initialization with configuration.""" + # Create temporary config file + with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: + config_data = { + 'keycloak': { + 'server_url': 'http://localhost:8080', + 'realm': 'test-realm', + 'client_id': 'test-client', + 'client_secret': 'test-secret' + }, + 'agent': { + 'name_prefix': 'test-sql-agent', + 'agent_type': 'python', + 'callback_url': 'http://localhost:8081', + 'api_url': 'http://localhost:8080', + 'heartbeat_interval': 30 + }, + 'llm': { + 'enabled': False + }, + 'database_url': 'sqlite:///test.db', + 'questions_file': None, + 'model_name': 'gpt-3.5-turbo' + } + yaml.dump(config_data, f) + config_path = f.name + + try: + # Create SQL agent (this will initialize but not start) + sql_agent = SQLAgent(config_path=config_path) + + # Verify initialization + self.assertEqual(sql_agent.name, 'SQL Agent') + self.assertIsNotNone(sql_agent.sentrius_agent) + self.assertIsNotNone(sql_agent.sql_config) + + finally: + # Clean up + os.unlink(config_path) + + @patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}) + @patch('agents.sql_agent.sql_agent.SQLDatabase') + @patch('agents.sql_agent.sql_agent.ChatOpenAI') + @patch('agents.sql_agent.sql_agent.SQLDatabaseSequentialChain') + def test_sql_agent_with_database_config(self, mock_chain, mock_llm, mock_db): + """Test SQL agent with database configuration.""" + # Mock the database components + mock_db_instance = Mock() + mock_db.from_uri.return_value = mock_db_instance + + mock_llm_instance = Mock() + mock_llm.return_value = mock_llm_instance + + mock_chain_instance = Mock() + mock_chain.from_llm.return_value = mock_chain_instance + + # Create temporary config file with database URL + with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: + config_data = { + 'keycloak': { + 'server_url': 'http://localhost:8080', + 'realm': 'test-realm', + 'client_id': 'test-client', + 'client_secret': 'test-secret' + }, + 'agent': { + 'name_prefix': 'test-sql-agent' + }, + 'llm': { + 'enabled': False + }, + 'database_url': 'sqlite:///test.db', + 'questions_file': None, + 'model_name': 'gpt-3.5-turbo' + } + yaml.dump(config_data, f) + config_path = f.name + + try: + # Create SQL agent + sql_agent = SQLAgent(config_path=config_path) + + # Verify database components were initialized + mock_db.from_uri.assert_called_once_with('sqlite:///test.db') + mock_llm.assert_called_once_with(model='gpt-3.5-turbo', openai_api_key='test-key') + mock_chain.from_llm.assert_called_once_with( + mock_llm_instance, mock_db_instance, verbose=True + ) + + finally: + # Clean up + os.unlink(config_path) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/python-agent/tests/test_services.py b/python-agent/tests/test_services.py new file mode 100644 index 00000000..4437b97e --- /dev/null +++ b/python-agent/tests/test_services.py @@ -0,0 +1,136 @@ +""" +Basic tests for Sentrius Python Agent services. +""" +import unittest +import os +import sys +from unittest.mock import Mock, patch, MagicMock + +# Add parent directory to Python path for imports +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from services.config import SentriusAgentConfig, KeycloakConfig, AgentConfig, LLMConfig +from services.keycloak_service import KeycloakService +from services.key_service import EphemeralKeyGen + + +class TestSentriusAgentConfig(unittest.TestCase): + """Test configuration management.""" + + def test_config_from_env(self): + """Test loading configuration from environment variables.""" + with patch.dict(os.environ, { + 'KEYCLOAK_SERVER_URL': 'http://test:8080', + 'KEYCLOAK_REALM': 'test-realm', + 'KEYCLOAK_CLIENT_ID': 'test-client', + 'KEYCLOAK_CLIENT_SECRET': 'test-secret', + 'AGENT_NAME_PREFIX': 'test-agent' + }): + config = SentriusAgentConfig.from_env() + + self.assertEqual(config.keycloak.server_url, 'http://test:8080') + self.assertEqual(config.keycloak.realm, 'test-realm') + self.assertEqual(config.keycloak.client_id, 'test-client') + self.assertEqual(config.keycloak.client_secret, 'test-secret') + self.assertEqual(config.agent.name_prefix, 'test-agent') + + def test_config_to_dict(self): + """Test configuration conversion to dictionary.""" + config = SentriusAgentConfig( + keycloak=KeycloakConfig( + server_url='http://test:8080', + realm='test-realm', + client_id='test-client', + client_secret='test-secret' + ), + agent=AgentConfig(name_prefix='test-agent'), + llm=LLMConfig() + ) + + config_dict = config.to_dict() + + self.assertIn('keycloak', config_dict) + self.assertIn('agent', config_dict) + self.assertIn('llm', config_dict) + self.assertEqual(config_dict['keycloak']['server_url'], 'http://test:8080') + + +class TestEphemeralKeyGen(unittest.TestCase): + """Test RSA key generation utilities.""" + + def test_generate_keypair(self): + """Test RSA key pair generation.""" + private_key, public_key = EphemeralKeyGen.generate_ephemeral_rsa_keypair() + + self.assertIsNotNone(private_key) + self.assertIsNotNone(public_key) + + def test_base64_public_key(self): + """Test base64 encoding of public key.""" + private_key, public_key = EphemeralKeyGen.generate_ephemeral_rsa_keypair() + base64_key = EphemeralKeyGen.get_base64_public_key(public_key) + + self.assertIsInstance(base64_key, str) + self.assertTrue(len(base64_key) > 0) + + def test_encrypt_decrypt(self): + """Test RSA encryption and decryption.""" + private_key, public_key = EphemeralKeyGen.generate_ephemeral_rsa_keypair() + test_data = "Hello, World!" + + # Encrypt with public key + encrypted_data = EphemeralKeyGen.encrypt_rsa_with_public_key(test_data, public_key) + + # Decrypt with private key + decrypted_data = EphemeralKeyGen.decrypt_rsa_with_private_key(encrypted_data, private_key) + + self.assertEqual(test_data, decrypted_data) + + +class TestKeycloakService(unittest.TestCase): + """Test Keycloak service functionality.""" + + def setUp(self): + """Set up test environment.""" + self.keycloak_service = KeycloakService( + server_url='http://test:8080', + realm='test-realm', + client_id='test-client', + client_secret='test-secret' + ) + + @patch('requests.post') + def test_get_keycloak_token(self, mock_post): + """Test getting Keycloak token.""" + # Mock successful response + mock_response = Mock() + mock_response.json.return_value = {'access_token': 'test-token'} + mock_response.raise_for_status.return_value = None + mock_post.return_value = mock_response + + token = self.keycloak_service.get_keycloak_token() + + self.assertEqual(token, 'test-token') + mock_post.assert_called_once() + + def test_extract_agent_id(self): + """Test extracting agent ID from JWT token.""" + # Create a mock JWT token (not signed, just for testing) + import base64 + import json + + header = {'typ': 'JWT', 'alg': 'RS256'} + payload = {'azp': 'test-agent-id', 'exp': 9999999999} + + header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=') + payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=') + + mock_token = f"{header_b64}.{payload_b64}.mock-signature" + + agent_id = self.keycloak_service.extract_agent_id(mock_token) + + self.assertEqual(agent_id, 'test-agent-id') + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 345a3c65e78aef49c13a95fd6cbc0a2a0f021ee3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:10:59 +0000 Subject: [PATCH 3/5] Add .gitignore for Python agent and remove cached files --- python-agent/.gitignore | 48 ++++++++++++++++++ .../__pycache__/__init__.cpython-312.pyc | Bin 164 -> 0 bytes .../agents/__pycache__/base.cpython-312.pyc | Bin 3649 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 174 -> 0 bytes .../__pycache__/sql_agent.cpython-312.pyc | Bin 5831 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 166 -> 0 bytes .../agent_client_service.cpython-312.pyc | Bin 8444 -> 0 bytes .../__pycache__/config.cpython-312.pyc | Bin 6868 -> 0 bytes .../__pycache__/key_service.cpython-312.pyc | Bin 5012 -> 0 bytes .../keycloak_service.cpython-312.pyc | Bin 7336 -> 0 bytes .../sentrius_agent.cpython-312.pyc | Bin 8654 -> 0 bytes python-agent/test.db | 0 12 files changed, 48 insertions(+) create mode 100644 python-agent/.gitignore delete mode 100644 python-agent/agents/__pycache__/__init__.cpython-312.pyc delete mode 100644 python-agent/agents/__pycache__/base.cpython-312.pyc delete mode 100644 python-agent/agents/sql_agent/__pycache__/__init__.cpython-312.pyc delete mode 100644 python-agent/agents/sql_agent/__pycache__/sql_agent.cpython-312.pyc delete mode 100644 python-agent/services/__pycache__/__init__.cpython-312.pyc delete mode 100644 python-agent/services/__pycache__/agent_client_service.cpython-312.pyc delete mode 100644 python-agent/services/__pycache__/config.cpython-312.pyc delete mode 100644 python-agent/services/__pycache__/key_service.cpython-312.pyc delete mode 100644 python-agent/services/__pycache__/keycloak_service.cpython-312.pyc delete mode 100644 python-agent/services/__pycache__/sentrius_agent.cpython-312.pyc delete mode 100644 python-agent/test.db diff --git a/python-agent/.gitignore b/python-agent/.gitignore new file mode 100644 index 00000000..d91333b0 --- /dev/null +++ b/python-agent/.gitignore @@ -0,0 +1,48 @@ +# Python cache files +__pycache__/ +*.py[cod] +*$py.class + +# Test databases +*.db + +# Virtual environments +venv/ +env/ +.env + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# Logs +*.log + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt \ No newline at end of file diff --git a/python-agent/agents/__pycache__/__init__.cpython-312.pyc b/python-agent/agents/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index c33e708d82b9a11964d83bfe2550358ce7d765e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 164 zcmX@j%ge<81RLf@Wq|0%AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd<*c8PpPQ;*RGOEU zTBKi|UzDvMoSIislv!GgU=&oAWaQ`RCZ+>r^uc7YetdjpUS>&ryk0@&FAkgB{FKt1 YRJ$TppqY$7Tnu7-WM*V!EMf+-0B0>J?EnA( diff --git a/python-agent/agents/__pycache__/base.cpython-312.pyc b/python-agent/agents/__pycache__/base.cpython-312.pyc deleted file mode 100644 index 3ce232540d47afa6c27576d6cd2a6cd4d376f6ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3649 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 diff --git a/python-agent/services/__pycache__/agent_client_service.cpython-312.pyc b/python-agent/services/__pycache__/agent_client_service.cpython-312.pyc deleted file mode 100644 index 4f22146f1e2c9a7f630fed2ba1240c1b9c0a3ae3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8444 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 diff --git a/python-agent/services/__pycache__/config.cpython-312.pyc b/python-agent/services/__pycache__/config.cpython-312.pyc deleted file mode 100644 index b590a4e6cc0a9f8e7e198dfc887b7faa00470494..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6868 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 GS^p0v*vOXv diff --git a/python-agent/services/__pycache__/keycloak_service.cpython-312.pyc b/python-agent/services/__pycache__/keycloak_service.cpython-312.pyc deleted file mode 100644 index bc8ee6434c5f2507ef670c701eea6d69d79fc71f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7336 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 VN{+$ztp8)pFbv$nkea^Ze*sfr%EbTx diff --git a/python-agent/services/__pycache__/sentrius_agent.cpython-312.pyc b/python-agent/services/__pycache__/sentrius_agent.cpython-312.pyc deleted file mode 100644 index 016f6ee34c92a8713d3c2f632cf883d52ea355ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8654 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 diff --git a/python-agent/test.db b/python-agent/test.db deleted file mode 100644 index e69de29b..00000000 From 6dff4661cbd185196d362a0be4fea956d7969263 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:57:53 +0000 Subject: [PATCH 4/5] Remove SQL agent as requested - all interactions through APIs only Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> --- python-agent/README.md | 30 ++-- python-agent/agents/sql_agent/__init__.py | 0 python-agent/agents/sql_agent/config.yaml | 26 ---- python-agent/agents/sql_agent/questions.yaml | 5 - python-agent/agents/sql_agent/sql_agent.py | 143 ------------------- python-agent/main.py | 31 +--- python-agent/requirements.txt | 6 - python-agent/tests/test_integration.py | 99 +------------ 8 files changed, 26 insertions(+), 314 deletions(-) delete mode 100644 python-agent/agents/sql_agent/__init__.py delete mode 100644 python-agent/agents/sql_agent/config.yaml delete mode 100644 python-agent/agents/sql_agent/questions.yaml delete mode 100644 python-agent/agents/sql_agent/sql_agent.py diff --git a/python-agent/README.md b/python-agent/README.md index 9cdaae2c..9b08fa11 100644 --- a/python-agent/README.md +++ b/python-agent/README.md @@ -24,7 +24,6 @@ The Python agent mirrors the Java agent architecture with these key components: ### Agent Framework - **BaseAgent**: Abstract base class for all agents -- **SQLAgent**: Example implementation for SQL operations ## Configuration @@ -65,14 +64,8 @@ AGENT_HEARTBEAT_INTERVAL=30 ## Usage -### Running the SQL Agent -```bash -# With configuration file -python main.py sql_agent --config config.yaml - -# With default configuration (uses environment variables) -python main.py sql_agent -``` +### Agent Framework +The Python agent provides a framework for creating custom agents that integrate with the Sentrius platform. All agents interact through APIs using JWT authentication, working with DTOs from the API and the LLM proxy. ### Creating Custom Agents ```python @@ -84,10 +77,26 @@ class MyCustomAgent(BaseAgent): def execute_task(self): # Your custom agent logic here + # Note: All data access is through Sentrius APIs, not direct database connections self.submit_provenance( event_type="CUSTOM_TASK", details={"task": "custom_operation"} ) + + def run(self): + # Start the agent lifecycle + self.start() + self.execute_task() + self.stop() +``` + +### Running Custom Agents +```bash +# With configuration file +python -c "from my_agent import MyCustomAgent; agent = MyCustomAgent('config.yaml'); agent.run()" + +# With environment variables +python -c "from my_agent import MyCustomAgent; agent = MyCustomAgent(); agent.run()" ``` ## API Operations @@ -115,7 +124,8 @@ The Python agent supports all the same API operations as the Java agent: - `PyJWT`: JWT token handling - `cryptography`: RSA key generation and encryption - `pyyaml`: YAML configuration parsing -- `langchain`: LLM integration (for SQL agent) + +Note: The Python agent accesses data through Sentrius APIs using DTOs and the LLM proxy, not through direct database connections. ## Installation diff --git a/python-agent/agents/sql_agent/__init__.py b/python-agent/agents/sql_agent/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/python-agent/agents/sql_agent/config.yaml b/python-agent/agents/sql_agent/config.yaml deleted file mode 100644 index d769e58e..00000000 --- a/python-agent/agents/sql_agent/config.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# SQL Agent Configuration -# Sentrius Agent Configuration -keycloak: - server_url: "http://localhost:8080" - realm: "sentrius" - client_id: "python-agent" - client_secret: "your-client-secret" - -agent: - name_prefix: "sql-agent" - agent_type: "python" - callback_url: "http://localhost:8081" - api_url: "http://localhost:8080" - heartbeat_interval: 30 - -llm: - enabled: false - provider: "openai" - model: "gpt-3.5-turbo" - api_key: null - endpoint: null - -# SQL-specific configuration -database_url: "postgresql://user:password@localhost:5432/database" -questions_file: "agents/sql_agent/questions.yaml" -model_name: "gpt-3.5-turbo" \ No newline at end of file diff --git a/python-agent/agents/sql_agent/questions.yaml b/python-agent/agents/sql_agent/questions.yaml deleted file mode 100644 index fbad4642..00000000 --- a/python-agent/agents/sql_agent/questions.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# Sample questions for SQL Agent -- "What are the top 5 customers by revenue?" -- "How many orders were placed last month?" -- "What is the average order value?" -- "Which products are running low on inventory?" \ No newline at end of file diff --git a/python-agent/agents/sql_agent/sql_agent.py b/python-agent/agents/sql_agent/sql_agent.py deleted file mode 100644 index ce5b4350..00000000 --- a/python-agent/agents/sql_agent/sql_agent.py +++ /dev/null @@ -1,143 +0,0 @@ -import yaml -import logging -import os -from langchain_community.chat_models import ChatOpenAI -from langchain_experimental.sql import SQLDatabaseSequentialChain -from langchain_community.utilities import SQLDatabase -from ..base import BaseAgent - -logger = logging.getLogger(__name__) - - -class SQLAgent(BaseAgent): - """SQL Agent using SQLDatabaseSequentialChain with Sentrius integration.""" - - def __init__(self, config_path: str = None): - # Load SQL-specific configuration - if config_path: - with open(config_path, "r") as file: - sql_config = yaml.safe_load(file) - else: - sql_config = {} - - super().__init__("SQL Agent", config_path=config_path) - - # Store SQL-specific configuration - self.sql_config = sql_config - self.db_url = sql_config.get("database_url") - self.questions_file = sql_config.get("questions_file") - self.model_name = sql_config.get("model_name", "gpt-4") - - # Initialize LangChain components if config is provided - if self.db_url: - self.db = SQLDatabase.from_uri(self.db_url) - try: - # Try to initialize LLM with API key - openai_api_key = os.getenv('OPENAI_API_KEY') - if openai_api_key: - self.llm = ChatOpenAI(model=self.model_name, openai_api_key=openai_api_key) - self.chain = SQLDatabaseSequentialChain.from_llm(self.llm, self.db, verbose=True) - else: - logger.warning("No OPENAI_API_KEY found, LLM features will be disabled") - self.llm = None - self.chain = None - except Exception as e: - logger.error(f"Failed to initialize LLM: {e}") - self.llm = None - self.chain = None - else: - self.db = None - self.llm = None - self.chain = None - logger.warning("No database URL provided, SQL operations will be limited") - - def execute_task(self): - """Execute the SQL Agent's specific task.""" - logger.info(f"Running {self.name}...") - - # Submit task start event - self.submit_provenance( - event_type="SQL_TASK_START", - details={ - "task_type": "sql_analysis", - "db_url": self.db_url, - "model_name": self.model_name, - "questions_file": self.questions_file - } - ) - - if not self.chain: - logger.error("SQL chain not initialized - missing database configuration") - self.submit_provenance( - event_type="SQL_TASK_ERROR", - details={ - "task_type": "sql_analysis", - "error": "SQL chain not initialized", - "error_type": "ConfigurationError" - } - ) - return - - # Load and process questions if questions file is provided - if self.questions_file: - try: - with open(self.questions_file, "r") as file: - questions = yaml.safe_load(file) - - logger.info(f"Running SQL Agent with {len(questions)} questions:") - - for idx, question in enumerate(questions, start=1): - logger.info(f"Question {idx}: {question}") - - try: - response = self.chain.run(question) - logger.info(f"Answer: {response}") - - # Submit successful query event - self.submit_provenance( - event_type="SQL_QUERY_SUCCESS", - details={ - "question_number": idx, - "question": question, - "response_length": len(str(response)) - } - ) - - except Exception as e: - logger.error(f"Failed to process question {idx}: {e}") - - # Submit query error event - self.submit_provenance( - event_type="SQL_QUERY_ERROR", - details={ - "question_number": idx, - "question": question, - "error": str(e), - "error_type": type(e).__name__ - } - ) - - except Exception as e: - logger.error(f"Failed to load questions file: {e}") - self.submit_provenance( - event_type="SQL_TASK_ERROR", - details={ - "task_type": "sql_analysis", - "error": f"Failed to load questions: {str(e)}", - "error_type": type(e).__name__ - } - ) - return - else: - logger.info("No questions file provided, SQL Agent ready for interactive use") - - # Submit task completion event - self.submit_provenance( - event_type="SQL_TASK_COMPLETE", - details={ - "task_type": "sql_analysis", - "status": "success" - } - ) - - logger.info("SQL Agent task execution completed") diff --git a/python-agent/main.py b/python-agent/main.py index ce3e0b6b..1bbf5fcb 100644 --- a/python-agent/main.py +++ b/python-agent/main.py @@ -1,7 +1,5 @@ import argparse import logging -import os -from agents.sql_agent.sql_agent import SQLAgent # Configure logging logging.basicConfig( @@ -15,7 +13,7 @@ def main(): parser = argparse.ArgumentParser(description="Run selected Sentrius Python agent.") parser.add_argument( "agent", - choices=["sql_agent"], + choices=[], # No agents available yet - SQL agent removed per feedback help="Select the agent to run." ) parser.add_argument( @@ -23,32 +21,13 @@ def main(): help="Path to agent configuration file", default=None ) - parser.add_argument( - "--sql-config", - help="Path to SQL-specific configuration file for SQL agent", - default="agents/sql_agent/config.yaml" - ) args = parser.parse_args() - try: - if args.agent == "sql_agent": - logger.info("Initializing SQL Agent with Sentrius integration...") - - # Use SQL-specific config if provided, otherwise fall back to main config - config_path = args.sql_config if os.path.exists(args.sql_config) else args.config - - sql_agent = SQLAgent(config_path=config_path) - sql_agent.run() - else: - logger.error("Unknown agent. Exiting.") - return 1 - - except Exception as e: - logger.error(f"Agent execution failed: {e}") - return 1 - - return 0 + # No agents available currently - SQL agent was removed + logger.error("No agents are currently available. SQL agent was removed as it won't have direct database access.") + logger.info("All interactions are through APIs once JWT is obtained, working via DTOs and LLM proxy.") + return 1 if __name__ == "__main__": diff --git a/python-agent/requirements.txt b/python-agent/requirements.txt index 93eba4cf..fd9ead33 100644 --- a/python-agent/requirements.txt +++ b/python-agent/requirements.txt @@ -1,11 +1,5 @@ argparse -langchain -langchain-community -langchain-experimental -sqlalchemy -openai pyyaml -psycopg2 requests>=2.25.0 PyJWT>=2.4.0 cryptography>=3.4.0 diff --git a/python-agent/tests/test_integration.py b/python-agent/tests/test_integration.py index fa3d437a..4c1a5dc4 100644 --- a/python-agent/tests/test_integration.py +++ b/python-agent/tests/test_integration.py @@ -4,16 +4,13 @@ import unittest import os import sys -from unittest.mock import Mock, patch, MagicMock -import tempfile -import yaml +from unittest.mock import Mock, patch # Add parent directory to Python path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from services.config import SentriusAgentConfig, KeycloakConfig, AgentConfig, LLMConfig from services.sentrius_agent import SentriusAgent -from agents.sql_agent.sql_agent import SQLAgent class TestSentriusAgentIntegration(unittest.TestCase): @@ -82,100 +79,6 @@ def test_sentrius_agent_start_stop(self, mock_agent_client, mock_keycloak): # Verify agent is stopped self.assertFalse(agent.running) - - def test_sql_agent_initialization_with_config(self): - """Test SQL agent initialization with configuration.""" - # Create temporary config file - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - config_data = { - 'keycloak': { - 'server_url': 'http://localhost:8080', - 'realm': 'test-realm', - 'client_id': 'test-client', - 'client_secret': 'test-secret' - }, - 'agent': { - 'name_prefix': 'test-sql-agent', - 'agent_type': 'python', - 'callback_url': 'http://localhost:8081', - 'api_url': 'http://localhost:8080', - 'heartbeat_interval': 30 - }, - 'llm': { - 'enabled': False - }, - 'database_url': 'sqlite:///test.db', - 'questions_file': None, - 'model_name': 'gpt-3.5-turbo' - } - yaml.dump(config_data, f) - config_path = f.name - - try: - # Create SQL agent (this will initialize but not start) - sql_agent = SQLAgent(config_path=config_path) - - # Verify initialization - self.assertEqual(sql_agent.name, 'SQL Agent') - self.assertIsNotNone(sql_agent.sentrius_agent) - self.assertIsNotNone(sql_agent.sql_config) - - finally: - # Clean up - os.unlink(config_path) - - @patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}) - @patch('agents.sql_agent.sql_agent.SQLDatabase') - @patch('agents.sql_agent.sql_agent.ChatOpenAI') - @patch('agents.sql_agent.sql_agent.SQLDatabaseSequentialChain') - def test_sql_agent_with_database_config(self, mock_chain, mock_llm, mock_db): - """Test SQL agent with database configuration.""" - # Mock the database components - mock_db_instance = Mock() - mock_db.from_uri.return_value = mock_db_instance - - mock_llm_instance = Mock() - mock_llm.return_value = mock_llm_instance - - mock_chain_instance = Mock() - mock_chain.from_llm.return_value = mock_chain_instance - - # Create temporary config file with database URL - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - config_data = { - 'keycloak': { - 'server_url': 'http://localhost:8080', - 'realm': 'test-realm', - 'client_id': 'test-client', - 'client_secret': 'test-secret' - }, - 'agent': { - 'name_prefix': 'test-sql-agent' - }, - 'llm': { - 'enabled': False - }, - 'database_url': 'sqlite:///test.db', - 'questions_file': None, - 'model_name': 'gpt-3.5-turbo' - } - yaml.dump(config_data, f) - config_path = f.name - - try: - # Create SQL agent - sql_agent = SQLAgent(config_path=config_path) - - # Verify database components were initialized - mock_db.from_uri.assert_called_once_with('sqlite:///test.db') - mock_llm.assert_called_once_with(model='gpt-3.5-turbo', openai_api_key='test-key') - mock_chain.from_llm.assert_called_once_with( - mock_llm_instance, mock_db_instance, verbose=True - ) - - finally: - # Clean up - os.unlink(config_path) if __name__ == '__main__': From 5941b76d39c05ce9cb6e0e8a03ef426d8d333f05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:13:11 +0000 Subject: [PATCH 5/5] Implement properties-based configuration system similar to Java agent Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> --- python-agent/README.md | 130 +++++++++++++++-- python-agent/agents/base.py | 131 +++++++++++------- python-agent/agents/chat_helper/__init__.py | 0 .../agents/chat_helper/chat_helper_agent.py | 83 +++++++++++ python-agent/application.properties | 47 +++++++ python-agent/chat-helper.yaml | 10 ++ python-agent/data-analyst.yaml | 10 ++ python-agent/main.py | 57 +++++++- python-agent/terminal-helper.yaml | 11 ++ python-agent/tests/test_config_manager.py | 125 +++++++++++++++++ python-agent/utils/config_manager.py | 125 +++++++++++++++++ 11 files changed, 657 insertions(+), 72 deletions(-) create mode 100644 python-agent/agents/chat_helper/__init__.py create mode 100644 python-agent/agents/chat_helper/chat_helper_agent.py create mode 100644 python-agent/application.properties create mode 100644 python-agent/chat-helper.yaml create mode 100644 python-agent/data-analyst.yaml create mode 100644 python-agent/terminal-helper.yaml create mode 100644 python-agent/tests/test_config_manager.py create mode 100644 python-agent/utils/config_manager.py diff --git a/python-agent/README.md b/python-agent/README.md index 9b08fa11..f8cfc432 100644 --- a/python-agent/README.md +++ b/python-agent/README.md @@ -27,7 +27,55 @@ The Python agent mirrors the Java agent architecture with these key components: ## Configuration -### YAML Configuration +The Python agent supports multiple configuration approaches, similar to the Java agent: + +### Properties-based Configuration (Recommended) + +Similar to the Java agent's `application.properties`, you can use a properties file that references agent-specific YAML files: + +**application.properties:** +```properties +# Keycloak Configuration +keycloak.realm=sentrius +keycloak.base-url=${KEYCLOAK_BASE_URL:http://localhost:8180} +keycloak.client-id=${KEYCLOAK_CLIENT_ID:python-agents} +keycloak.client-secret=${KEYCLOAK_CLIENT_SECRET:your-client-secret} + +# Agent Configuration +agent.name.prefix=python-agent +agent.type=python +agent.callback.url=${AGENT_CALLBACK_URL:http://localhost:8093} +agent.api.url=${AGENT_API_URL:http://localhost:8080/} + +# Agent Definitions - these reference YAML files that define agent behavior +agent.chat.helper.config=chat-helper.yaml +agent.chat.helper.enabled=true + +agent.data.analyst.config=data-analyst.yaml +agent.data.analyst.enabled=false +``` + +**chat-helper.yaml:** +```yaml +description: "Agent that handles chat interactions and provides helpful responses via OpenAI integration." +context: | + You are a helpful agent that is responding to users via a chat interface. Please respond to the user in a friendly and helpful manner. + Return responses in the following format: + { + "previousOperation": "", + "nextOperation": "", + "terminalSummaryForLLM": "", + "responseForUser": "" + } +``` + +### Environment Variable Substitution + +Properties support environment variable substitution using the `${VARIABLE:default}` syntax: +- `${KEYCLOAK_BASE_URL:http://localhost:8180}` - Uses `KEYCLOAK_BASE_URL` env var or defaults to `http://localhost:8180` +- `${KEYCLOAK_CLIENT_ID:python-agents}` - Uses `KEYCLOAK_CLIENT_ID` env var or defaults to `python-agents` + +### Legacy YAML Configuration ```yaml keycloak: server_url: "http://localhost:8080" @@ -64,6 +112,35 @@ AGENT_HEARTBEAT_INTERVAL=30 ## Usage +### Running Agents + +#### With Properties Configuration +```bash +# Run chat helper agent using default application.properties +python main.py chat-helper + +# Run with custom configuration file +python main.py chat-helper --config my-app.properties + +# Run with task data +python main.py chat-helper --task-data '{"message": "Hello, how can you help?"}' + +# Test mode (no external services) +TEST_MODE=true python main.py chat-helper +``` + +#### Environment Variable Configuration +```bash +# Set environment variables +export KEYCLOAK_BASE_URL=http://localhost:8180 +export KEYCLOAK_CLIENT_ID=python-agents +export KEYCLOAK_CLIENT_SECRET=your-secret +export TEST_MODE=false + +# Run agent +python main.py chat-helper +``` + ### Agent Framework The Python agent provides a framework for creating custom agents that integrate with the Sentrius platform. All agents interact through APIs using JWT authentication, working with DTOs from the API and the LLM proxy. @@ -72,31 +149,54 @@ The Python agent provides a framework for creating custom agents that integrate from agents.base import BaseAgent class MyCustomAgent(BaseAgent): - def __init__(self, config_path=None): - super().__init__("My Custom Agent", config_path=config_path) + def __init__(self, config_manager): + super().__init__(config_manager, name="my-custom-agent") + # Load agent-specific configuration + self.agent_definition = config_manager.get_agent_definition('my.custom') - def execute_task(self): + def execute_task(self, task_data=None): # Your custom agent logic here # Note: All data access is through Sentrius APIs, not direct database connections self.submit_provenance( event_type="CUSTOM_TASK", - details={"task": "custom_operation"} + details={"task": "custom_operation", "data": task_data} ) - def run(self): - # Start the agent lifecycle - self.start() - self.execute_task() - self.stop() + # Return structured response + return { + "status": "completed", + "result": "Custom task executed successfully" + } +``` + +**my-custom.yaml:** +```yaml +description: "Custom agent that performs specialized tasks" +context: | + You are a custom agent designed to handle specific business logic. + Process requests according to your specialized capabilities. +``` + +**Add to application.properties:** +```properties +agent.my.custom.config=my-custom.yaml +agent.my.custom.enabled=true ``` ### Running Custom Agents -```bash -# With configuration file -python -c "from my_agent import MyCustomAgent; agent = MyCustomAgent('config.yaml'); agent.run()" +```python +# Add to main.py AVAILABLE_AGENTS dict +from agents.my_custom.my_custom_agent import MyCustomAgent -# With environment variables -python -c "from my_agent import MyCustomAgent; agent = MyCustomAgent(); agent.run()" +AVAILABLE_AGENTS = { + 'chat-helper': ChatHelperAgent, + 'my-custom': MyCustomAgent, # Add your agent here +} +``` + +```bash +# Run your custom agent +python main.py my-custom --task-data '{"operation": "process_data"}' ``` ## API Operations diff --git a/python-agent/agents/base.py b/python-agent/agents/base.py index 7abd5d0e..a3a80c37 100644 --- a/python-agent/agents/base.py +++ b/python-agent/agents/base.py @@ -11,74 +11,103 @@ class BaseAgent(ABC): """Abstract base class for all agents with Sentrius API integration.""" - def __init__(self, name: str, config_path: Optional[str] = None, config: Optional[SentriusAgentConfig] = None): - self.name = name + def __init__(self, config_manager, name: Optional[str] = None): + self.config_manager = config_manager + self.name = name or self.__class__.__name__.lower().replace('agent', '') - # Load configuration - if config: - self.config = config - elif config_path: - self.config = SentriusAgentConfig.from_yaml(config_path) - else: - self.config = SentriusAgentConfig.from_env() + # Check if we're in test mode + self.test_mode = config_manager.get_property('test.mode', 'false').lower() == 'true' - # Initialize Sentrius agent - self.sentrius_agent = SentriusAgent(self.config) + if not self.test_mode: + # Load configuration for Sentrius integration + agent_config = config_manager.get_agent_config() + keycloak_config = config_manager.get_keycloak_config() + + # Create SentriusAgentConfig from the loaded configuration + self.config = SentriusAgentConfig( + keycloak_server_url=keycloak_config['server_url'], + keycloak_realm=keycloak_config['realm'], + keycloak_client_id=keycloak_config['client_id'], + keycloak_client_secret=keycloak_config['client_secret'], + agent_name_prefix=agent_config['name_prefix'], + agent_type=agent_config['agent_type'], + agent_callback_url=agent_config['callback_url'], + api_url=agent_config['api_url'], + heartbeat_interval=agent_config['heartbeat_interval'] + ) + + # Initialize Sentrius agent + self.sentrius_agent = SentriusAgent(self.config) + else: + logger.info("Running in test mode - external services disabled") + self.sentrius_agent = None - logger.info(f"Initialized {self.__class__.__name__}: {self.name}") + logger.info(f"Initialized {self.__class__.__name__}: {config_manager}") @abstractmethod - def execute_task(self): + def execute_task(self, task_data: Optional[Dict[str, Any]] = None): """Method to execute the agent's specific task.""" pass - def run(self): + def run(self, task_data: Optional[Dict[str, Any]] = None): """Main run method that handles agent lifecycle.""" try: - with self.sentrius_agent: - logger.info(f"Starting {self.name} agent") - - # Submit start event - self.sentrius_agent.submit_provenance_event( - event_type="AGENT_START", - details={ - "agent_name": self.name, - "agent_class": self.__class__.__name__ - } - ) - - # Execute the specific task - self.execute_task() - - # Submit completion event - self.sentrius_agent.submit_provenance_event( - event_type="AGENT_COMPLETE", - details={ - "agent_name": self.name, - "status": "completed" - } - ) - - logger.info(f"{self.name} agent completed successfully") + if self.sentrius_agent and not self.test_mode: + with self.sentrius_agent: + logger.info(f"Starting {self.name} agent") + + # Submit start event + self.sentrius_agent.submit_provenance_event( + event_type="AGENT_START", + details={ + "agent_name": self.name, + "agent_class": self.__class__.__name__ + } + ) + + # Execute the specific task + result = self.execute_task(task_data) + + # Submit completion event + self.sentrius_agent.submit_provenance_event( + event_type="AGENT_COMPLETE", + details={ + "agent_name": self.name, + "status": "completed" + } + ) + + logger.info(f"{self.name} agent completed successfully") + return result + else: + # Test mode - just execute the task + logger.info(f"Starting {self.name} agent (test mode)") + result = self.execute_task(task_data) + logger.info(f"{self.name} agent completed successfully (test mode)") + return result except Exception as e: logger.error(f"{self.name} agent failed: {e}") # Submit error event - try: - self.sentrius_agent.submit_provenance_event( - event_type="AGENT_ERROR", - details={ - "agent_name": self.name, - "error": str(e), - "error_type": type(e).__name__ - } - ) - except: - pass # Don't fail if we can't submit error event + if self.sentrius_agent and not self.test_mode: + try: + self.sentrius_agent.submit_provenance_event( + event_type="AGENT_ERROR", + details={ + "agent_name": self.name, + "error": str(e), + "error_type": type(e).__name__ + } + ) + except: + pass # Don't fail if we can't submit error event raise def submit_provenance(self, event_type: str, details: Dict[str, Any]): """Submit a provenance event.""" - self.sentrius_agent.submit_provenance_event(event_type, details) + if self.sentrius_agent and not self.test_mode: + self.sentrius_agent.submit_provenance_event(event_type, details) + else: + logger.info(f"Test mode - would submit provenance: {event_type} - {details}") diff --git a/python-agent/agents/chat_helper/__init__.py b/python-agent/agents/chat_helper/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python-agent/agents/chat_helper/chat_helper_agent.py b/python-agent/agents/chat_helper/chat_helper_agent.py new file mode 100644 index 00000000..83f559f0 --- /dev/null +++ b/python-agent/agents/chat_helper/chat_helper_agent.py @@ -0,0 +1,83 @@ +""" +Chat Helper Agent - Provides conversational AI assistance. +""" +import logging +from typing import Dict, Any, Optional +from agents.base import BaseAgent + +logger = logging.getLogger(__name__) + + +class ChatHelperAgent(BaseAgent): + """Agent that provides chat-based assistance using LLM integration.""" + + def __init__(self, config_manager): + super().__init__(config_manager) + self.agent_definition = config_manager.get_agent_definition('chat.helper') + if not self.agent_definition: + raise ValueError("Chat helper agent configuration not found") + + logger.info(f"Initialized ChatHelperAgent: {self.agent_definition.get('description', 'No description')}") + + def execute_task(self, task_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Execute chat helper task.""" + try: + # Submit provenance for task start + self.submit_provenance("CHAT_TASK_START", { + "agent_type": "chat-helper", + "task_data": task_data + }) + + # Process the chat request (this would integrate with LLM) + response = self._process_chat_request(task_data) + + # Submit provenance for task completion + self.submit_provenance("CHAT_TASK_COMPLETE", { + "agent_type": "chat-helper", + "response": response + }) + + return response + + except Exception as e: + logger.error(f"Error executing chat helper task: {e}") + self.submit_provenance("CHAT_TASK_ERROR", { + "agent_type": "chat-helper", + "error": str(e) + }) + raise + + def _process_chat_request(self, task_data: Optional[Dict[str, Any]]) -> Dict[str, Any]: + """Process chat request using agent context and LLM.""" + if not task_data: + return { + "previousOperation": "initialization", + "nextOperation": "waiting_for_user_input", + "terminalSummaryForLLM": "Chat helper agent initialized and ready", + "responseForUser": "Hello! I'm your chat helper agent. How can I assist you today?" + } + + user_message = task_data.get('message', '') + context = self.agent_definition.get('context', '') + + # This would integrate with the LLM service + # For now, return a structured response based on the agent's context + return { + "previousOperation": "user_message_received", + "nextOperation": "generate_response", + "terminalSummaryForLLM": f"User asked: {user_message}", + "responseForUser": f"I received your message: '{user_message}'. I'm a helpful chat assistant ready to help!" + } + + def get_agent_info(self) -> Dict[str, Any]: + """Get information about this agent.""" + return { + "name": "chat-helper", + "type": "conversational", + "description": self.agent_definition.get('description', ''), + "capabilities": [ + "conversational_ai", + "user_assistance", + "structured_responses" + ] + } \ No newline at end of file diff --git a/python-agent/application.properties b/python-agent/application.properties new file mode 100644 index 00000000..ddce1bc8 --- /dev/null +++ b/python-agent/application.properties @@ -0,0 +1,47 @@ +# Sentrius Python Agent Configuration + +# Test Mode - set to true to disable external service connections +test.mode=${TEST_MODE:false} + +# Keycloak Configuration +keycloak.realm=sentrius +keycloak.base-url=${KEYCLOAK_BASE_URL:http://localhost:8180} +keycloak.client-id=${KEYCLOAK_CLIENT_ID:python-agents} +keycloak.client-secret=${KEYCLOAK_CLIENT_SECRET:your-client-secret} + +# Agent Configuration +agent.name.prefix=python-agent +agent.type=python +agent.callback.url=${AGENT_CALLBACK_URL:http://localhost:8093} +agent.api.url=${AGENT_API_URL:http://localhost:8080/} +agent.heartbeat.interval=30 + +# LLM Configuration +agent.llm.endpoint=${LLM_ENDPOINT:http://localhost:8084/} +agent.llm.enabled=true + +# Agent Definitions - these reference YAML files that define agent behavior +agent.chat.helper.config=chat-helper.yaml +agent.chat.helper.enabled=true + +agent.data.analyst.config=data-analyst.yaml +agent.data.analyst.enabled=false + +agent.terminal.helper.config=terminal-helper.yaml +agent.terminal.helper.enabled=false + +# OpenTelemetry Configuration +otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} +otel.traces.exporter=otlp +otel.exporter.otlp.protocol=grpc +otel.metrics.exporter=none +otel.logs.exporter=none +otel.resource.attributes.service.name=python-agent +otel.traces.sampler=always_on +otel.exporter.otlp.timeout=10s + +# Provenance Configuration +provenance.kafka.topic=sentrius-provenance + +# Logging Configuration +logging.level=INFO \ No newline at end of file diff --git a/python-agent/chat-helper.yaml b/python-agent/chat-helper.yaml new file mode 100644 index 00000000..f8ecd04f --- /dev/null +++ b/python-agent/chat-helper.yaml @@ -0,0 +1,10 @@ +description: "Agent that handles chat interactions and provides helpful responses via OpenAI integration." +context: | + You are a helpful agent that is responding to users via a chat interface. Please respond to the user in a friendly and helpful manner. + Return responses in the following format: + { + "previousOperation": "", + "nextOperation": "", + "terminalSummaryForLLM": "", + "responseForUser": "" + } \ No newline at end of file diff --git a/python-agent/data-analyst.yaml b/python-agent/data-analyst.yaml new file mode 100644 index 00000000..cdf07292 --- /dev/null +++ b/python-agent/data-analyst.yaml @@ -0,0 +1,10 @@ +description: "Agent that provides data analysis and insights using AI capabilities." +context: | + You are a data analyst agent that helps users understand and analyze data. You can: + - Analyze datasets and provide insights + - Generate reports and summaries + - Suggest data visualization approaches + - Answer questions about data patterns and trends + + Always provide clear, actionable insights and suggest next steps for data exploration. + Return responses in a structured format that includes key findings and recommendations. \ No newline at end of file diff --git a/python-agent/main.py b/python-agent/main.py index 1bbf5fcb..f8de1a3a 100644 --- a/python-agent/main.py +++ b/python-agent/main.py @@ -1,5 +1,13 @@ import argparse import logging +import sys +from pathlib import Path + +# Add the current directory to the Python path +sys.path.append(str(Path(__file__).parent)) + +from utils.config_manager import ConfigManager +from agents.chat_helper.chat_helper_agent import ChatHelperAgent # Configure logging logging.basicConfig( @@ -8,26 +16,63 @@ ) logger = logging.getLogger(__name__) +AVAILABLE_AGENTS = { + 'chat-helper': ChatHelperAgent, +} + def main(): parser = argparse.ArgumentParser(description="Run selected Sentrius Python agent.") parser.add_argument( "agent", - choices=[], # No agents available yet - SQL agent removed per feedback + choices=list(AVAILABLE_AGENTS.keys()), help="Select the agent to run." ) parser.add_argument( "--config", - help="Path to agent configuration file", + help="Path to agent configuration properties file", + default="application.properties" + ) + parser.add_argument( + "--task-data", + help="JSON string with task data for the agent", default=None ) args = parser.parse_args() - # No agents available currently - SQL agent was removed - logger.error("No agents are currently available. SQL agent was removed as it won't have direct database access.") - logger.info("All interactions are through APIs once JWT is obtained, working via DTOs and LLM proxy.") - return 1 + try: + # Load configuration + config_manager = ConfigManager(args.config) + + # Check if the requested agent is enabled + if not config_manager.is_agent_enabled(args.agent.replace('-', '.')): + logger.error(f"Agent '{args.agent}' is not enabled in configuration") + return 1 + + # Initialize and run the agent + agent_class = AVAILABLE_AGENTS[args.agent] + agent = agent_class(config_manager) + + logger.info(f"Starting {args.agent} agent...") + + # Parse task data if provided + task_data = None + if args.task_data: + import json + task_data = json.loads(args.task_data) + + # Execute the agent task + result = agent.execute_task(task_data) + + logger.info(f"Agent execution completed successfully") + logger.info(f"Result: {result}") + + return 0 + + except Exception as e: + logger.error(f"Error running agent: {e}") + return 1 if __name__ == "__main__": diff --git a/python-agent/terminal-helper.yaml b/python-agent/terminal-helper.yaml new file mode 100644 index 00000000..9a90c3c6 --- /dev/null +++ b/python-agent/terminal-helper.yaml @@ -0,0 +1,11 @@ +description: "Agent that assists with terminal operations and command-line tasks." +context: | + You are a terminal helper agent that assists users with command-line operations and system administration tasks. + You can help with: + - Explaining command-line tools and their usage + - Suggesting appropriate commands for specific tasks + - Troubleshooting system issues + - Automating repetitive terminal operations + + Always prioritize security and best practices when suggesting commands. + Provide clear explanations of what commands do before suggesting them. \ No newline at end of file diff --git a/python-agent/tests/test_config_manager.py b/python-agent/tests/test_config_manager.py new file mode 100644 index 00000000..74a6d756 --- /dev/null +++ b/python-agent/tests/test_config_manager.py @@ -0,0 +1,125 @@ +""" +Test configuration manager functionality. +""" +import unittest +import tempfile +import os +from pathlib import Path +from utils.config_manager import ConfigManager + + +class TestConfigManager(unittest.TestCase): + """Test the ConfigManager class.""" + + def setUp(self): + """Set up test configuration files.""" + # Create temporary directory for test files + self.test_dir = tempfile.mkdtemp() + self.properties_file = os.path.join(self.test_dir, "test.properties") + self.yaml_file = os.path.join(self.test_dir, "test-agent.yaml") + + # Create test properties file + properties_content = """ +# Test properties +test.mode=true +keycloak.realm=test-realm +keycloak.base-url=${KEYCLOAK_URL:http://localhost:8180} +agent.name.prefix=test-agent +agent.test.config=test-agent.yaml +agent.test.enabled=true +""" + with open(self.properties_file, 'w') as f: + f.write(properties_content) + + # Create test YAML file + yaml_content = """ +description: "Test agent for unit tests" +context: | + You are a test agent used for unit testing. + Always return structured responses for testing. +""" + with open(self.yaml_file, 'w') as f: + f.write(yaml_content) + + def tearDown(self): + """Clean up test files.""" + import shutil + shutil.rmtree(self.test_dir) + + def test_load_properties(self): + """Test loading properties file.""" + config_manager = ConfigManager(self.properties_file) + + self.assertEqual(config_manager.get_property('test.mode'), 'true') + self.assertEqual(config_manager.get_property('keycloak.realm'), 'test-realm') + self.assertEqual(config_manager.get_property('agent.name.prefix'), 'test-agent') + + def test_env_var_substitution(self): + """Test environment variable substitution.""" + # Set environment variable + os.environ['KEYCLOAK_URL'] = 'http://test.example.com:8080' + + config_manager = ConfigManager(self.properties_file) + + # Should use environment variable + self.assertEqual(config_manager.get_property('keycloak.base-url'), 'http://test.example.com:8080') + + # Clean up + del os.environ['KEYCLOAK_URL'] + + def test_env_var_default(self): + """Test environment variable default values.""" + config_manager = ConfigManager(self.properties_file) + + # Should use default value when env var is not set + self.assertEqual(config_manager.get_property('keycloak.base-url'), 'http://localhost:8180') + + def test_agent_config_loading(self): + """Test loading agent configuration.""" + # Change to test directory so relative paths work + original_cwd = os.getcwd() + os.chdir(self.test_dir) + + try: + config_manager = ConfigManager("test.properties") + + agent_config = config_manager.get_agent_definition('test') + self.assertIsNotNone(agent_config) + self.assertEqual(agent_config['description'], 'Test agent for unit tests') + self.assertIn('You are a test agent', agent_config['context']) + finally: + os.chdir(original_cwd) + + def test_enabled_agents(self): + """Test getting enabled agents.""" + config_manager = ConfigManager(self.properties_file) + + enabled_agents = config_manager.get_enabled_agents() + self.assertIn('test', enabled_agents) + + self.assertTrue(config_manager.is_agent_enabled('test')) + self.assertFalse(config_manager.is_agent_enabled('nonexistent')) + + def test_get_configs(self): + """Test getting different configuration sections.""" + config_manager = ConfigManager(self.properties_file) + + keycloak_config = config_manager.get_keycloak_config() + self.assertEqual(keycloak_config['realm'], 'test-realm') + self.assertEqual(keycloak_config['server_url'], 'http://localhost:8180') + + agent_config = config_manager.get_agent_config() + self.assertEqual(agent_config['name_prefix'], 'test-agent') + + def test_missing_files(self): + """Test handling of missing configuration files.""" + # Test with non-existent properties file + config_manager = ConfigManager('/nonexistent/file.properties') + + # Should not crash and should return defaults + self.assertEqual(len(config_manager.properties), 0) + self.assertEqual(config_manager.get_property('missing.key', 'default'), 'default') + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/python-agent/utils/config_manager.py b/python-agent/utils/config_manager.py new file mode 100644 index 00000000..e04e32a1 --- /dev/null +++ b/python-agent/utils/config_manager.py @@ -0,0 +1,125 @@ +""" +Configuration management for the Sentrius Python Agent. +Supports loading from properties files and YAML files, similar to Java Spring configuration. +""" +import os +import yaml +import logging +from typing import Dict, Any, Optional, List +from pathlib import Path + +logger = logging.getLogger(__name__) + + +class ConfigManager: + """Manages configuration loading from properties and YAML files.""" + + def __init__(self, properties_file: str = "application.properties"): + self.properties_file = properties_file + self.properties = {} + self.agent_configs = {} + self._load_properties() + self._load_agent_configs() + + def _load_properties(self): + """Load configuration from properties file with environment variable substitution.""" + properties_path = Path(self.properties_file) + if not properties_path.exists(): + logger.warning(f"Properties file {self.properties_file} not found") + return + + try: + with open(properties_path, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + if '=' in line: + key, value = line.split('=', 1) + # Handle environment variable substitution + value = self._substitute_env_vars(value) + self.properties[key.strip()] = value.strip() + + logger.info(f"Loaded {len(self.properties)} properties from {self.properties_file}") + except Exception as e: + logger.error(f"Error loading properties file: {e}") + + def _substitute_env_vars(self, value: str) -> str: + """Substitute environment variables in property values.""" + # Handle ${VARIABLE:default} pattern + import re + pattern = r'\$\{([^:}]+):([^}]*)\}' + + def replacer(match): + env_var = match.group(1) + default_val = match.group(2) + return os.getenv(env_var, default_val) + + return re.sub(pattern, replacer, value) + + def _load_agent_configs(self): + """Load agent-specific configuration files referenced in properties.""" + agent_config_keys = [k for k in self.properties.keys() if k.endswith('.config')] + + for config_key in agent_config_keys: + yaml_file = self.properties[config_key] + agent_name = config_key.replace('.config', '').replace('agent.', '') + + try: + yaml_path = Path(yaml_file) + if yaml_path.exists(): + with open(yaml_path, 'r') as f: + config = yaml.safe_load(f) + self.agent_configs[agent_name] = config + logger.info(f"Loaded agent config for {agent_name} from {yaml_file}") + else: + logger.warning(f"Agent config file {yaml_file} not found for {agent_name}") + except Exception as e: + logger.error(f"Error loading agent config {yaml_file}: {e}") + + def get_property(self, key: str, default: Any = None) -> Any: + """Get a property value with optional default.""" + return self.properties.get(key, default) + + def get_keycloak_config(self) -> Dict[str, str]: + """Get Keycloak configuration.""" + return { + 'server_url': self.get_property('keycloak.base-url', 'http://localhost:8180'), + 'realm': self.get_property('keycloak.realm', 'sentrius'), + 'client_id': self.get_property('keycloak.client-id', 'python-agents'), + 'client_secret': self.get_property('keycloak.client-secret') + } + + def get_agent_config(self) -> Dict[str, Any]: + """Get agent configuration.""" + return { + 'name_prefix': self.get_property('agent.name.prefix', 'python-agent'), + 'agent_type': self.get_property('agent.type', 'python'), + 'callback_url': self.get_property('agent.callback.url', 'http://localhost:8093'), + 'api_url': self.get_property('agent.api.url', 'http://localhost:8080/'), + 'heartbeat_interval': int(self.get_property('agent.heartbeat.interval', '30')) + } + + def get_llm_config(self) -> Dict[str, Any]: + """Get LLM configuration.""" + return { + 'endpoint': self.get_property('agent.llm.endpoint', 'http://localhost:8084/'), + 'enabled': self.get_property('agent.llm.enabled', 'true').lower() == 'true' + } + + def get_agent_definition(self, agent_name: str) -> Optional[Dict[str, Any]]: + """Get agent definition from loaded YAML configs.""" + return self.agent_configs.get(agent_name) + + def get_enabled_agents(self) -> List[str]: + """Get list of enabled agents.""" + enabled_agents = [] + for key, value in self.properties.items(): + if key.endswith('.enabled') and value.lower() == 'true': + agent_name = key.replace('.enabled', '').replace('agent.', '') + enabled_agents.append(agent_name) + return enabled_agents + + def is_agent_enabled(self, agent_name: str) -> bool: + """Check if a specific agent is enabled.""" + enabled_key = f'agent.{agent_name}.enabled' + return self.get_property(enabled_key, 'false').lower() == 'true' \ No newline at end of file