From 4caf1a90b1a1ff6b7721dab532087c819cc14bcf Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Sun, 14 Dec 2025 20:59:31 -0800 Subject: [PATCH 01/17] Add files via upload --- ARCHITECTURE.md | 78 +++++++++++++----- __pycache__/app.cpython-311.pyc | Bin 0 -> 8252 bytes __pycache__/app.cpython-314.pyc | Bin 0 -> 7099 bytes app.py | 137 ++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 22 deletions(-) create mode 100644 __pycache__/app.cpython-311.pyc create mode 100644 __pycache__/app.cpython-314.pyc create mode 100644 app.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 223b4737d..a594c85c1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,24 +1,58 @@ -## 2. `ARCHITECTURE.md` +# CIFR Agent System – Architecture + +This document aligns the project with the official Agentic AI Hackathon template ([odsc2015/agentic-hackathon-template](https://github.com/odsc2015/agentic-hackathon-template)). + +## High-Level Overview +- Goal: Reduce intercompany friction through multi-agent collaboration. +- Required Gemini usage: google.genai for multimodal analysis, planning, and interventions. +- Deployment target: local demo; cloud-ready via GCP. + +## Component Diagram (ASCII) +``` +User Request + │ + ▼ + Planner (Gemini plan) + │ plan + ▼ + Executor ───────────────┐ + │ executes │ + ▼ │ + CommunicationAgent ──► KnowledgeAgent (shared memory) + │ │ + ▼ │ + FrictionDetectionAgent ◄──┘ + │ + ▼ + InterventionAgent → User-facing guidance +``` + +## Modules +- `src/planner.py`: Breaks user goals into sub-tasks via Gemini text model. +- `src/executor.py`: Coordinates planner output with existing agents and tracks trace/logs. +- `src/memory.py`: Lightweight in-memory log; complements `KnowledgeAgent`. +- `cifr_agent_system/*`: Original specialized agents (communication, friction, intervention, knowledge, utils). + +## Data Flow +1) User goal enters Planner → structured steps. +2) Executor routes steps: + - Analyze messages (CommunicationAgent + Gemini Vision/Text). + - Detect misalignment (FrictionDetectionAgent + Gemini). + - Generate interventions (InterventionAgent + Gemini). +3) KnowledgeAgent stores all analyses and interventions. +4) Executor returns combined trace/results for demo or API. + +## Environment & Secrets +- `.env` (not committed): `GCP_PROJECT_ID`, `GCP_LOCATION`, `GOOGLE_API_KEY`, `GEMINI_PRO_MODEL_ID`, `GEMINI_PRO_VISION_MODEL_ID`. +- `key.json` must not be committed; rotate/remove from history if already tracked. + +## Observability (current) +- CLI prints and lightweight metrics in `main.py`. +- Web dashboard (Flask) optional for live metrics. + +## Extension Ideas +- Persist memory to a database (Firestore/Spanner). +- Add tool router for external APIs (search, calendars). +- Add tracing (OpenTelemetry) and structured logging for demo. -```markdown -# Architecture Overview - -Below, sketch (ASCII, hand-drawn JPEG/PNG pasted in, or ASCII art) the high-level components of your agent. - -## Components - -1. **User Interface** - - E.g., Streamlit, CLI, Slack bot - -2. **Agent Core** - - **Planner**: how you break down tasks - - **Executor**: LLM prompt + tool-calling logic - - **Memory**: vector store, cache, or on-disk logs - -3. **Tools / APIs** - - E.g., Google Gemini API, Tools, etc - -4. **Observability** - - Logging of each reasoning step - - Error handling / retries diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3752396e7748b5fe78f31c35fc9900474f4cee59 GIT binary patch literal 8252 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" + +# Import agents and Config from your cifr_agent_system package +from cifr_agent_system.config import Config +from cifr_agent_system.communication_agent import CommunicationAgent +from cifr_agent_system.knowledge_agent import KnowledgeAgent +from cifr_agent_system.friction_detection_agent import FrictionDetectionAgent +from cifr_agent_system.intervention_agent import InterventionAgent +from cifr_agent_system.utils import generate_unique_id + +app = Flask(__name__, + static_folder='./frontend/static', + template_folder='./frontend/templates') + +# Initialize agents globally or per-request if state management is complex +# For simplicity, we'll initialize them globally here. +knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) +communication_agent = CommunicationAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) +friction_detection_agent = FrictionDetectionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) +intervention_agent = InterventionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, friction_detection_agent=friction_detection_agent, location=Config.GCP_LOCATION) + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/api/process_message', methods=['POST']) +def process_message_api(): + data = request.form + text_content = data.get('text_content', '') + image_file = request.files.get('image_file') + + image_bytes = None + if image_file: + image_bytes = image_file.read() + + message_id = generate_unique_id("web_message") + timestamp = datetime.now().isoformat() + + sample_message = { + "message_id": message_id, + "text_content": text_content, + "image_bytes": image_bytes, + "timestamp": timestamp, + "sender": "Web User" + } + + print(f"[API] Processing message ID: {message_id}") + + results = { + "original_message": sample_message, + "communication_analysis": None, + "knowledge_update_status": None, + "friction_detection": None, + "intervention_suggestion": None, + "error": None + } + + try: + # 1. Communication Agent processing + comm_agent_results = communication_agent.process_collaboration_message(sample_message) + results["communication_analysis"] = serialize_google_cloud_object(comm_agent_results) + results["knowledge_update_status"] = "Context stored under 'communication_analysis_{}'".format(message_id) + + # 2. Friction Detection + friction_results = friction_detection_agent.detect_communication_friction(f"communication_analysis_{message_id}") + results["friction_detection"] = serialize_google_cloud_object(friction_results) + + # 3. Intervention Suggestion + intervention_suggestion = intervention_agent.suggest_intervention(f"communication_analysis_{message_id}") + results["intervention_suggestion"] = serialize_google_cloud_object(intervention_suggestion) + + except Exception as e: + results["error"] = str(e) + print(f"[API Error] {e}") + + return jsonify(results) + +if __name__ == '__main__': + # It's recommended to run Flask in development mode for easier debugging. + # For production, use a production-ready WSGI server like Gunicorn or uWSGI. + app.run(debug=True, host='0.0.0.0', port=5000) + From b3bc832eb276b66cb724b13ba2d746b33870b816 Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:01:17 -0800 Subject: [PATCH 02/17] Add files via upload --- src/__init__.py | 3 + src/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 155 bytes src/__pycache__/executor.cpython-311.pyc | Bin 0 -> 4428 bytes src/__pycache__/memory.cpython-311.pyc | Bin 0 -> 2235 bytes src/__pycache__/planner.cpython-311.pyc | Bin 0 -> 3795 bytes src/executor.py | 72 +++++++++++++++++++++ src/memory.py | 28 +++++++++ src/planner.py | 76 +++++++++++++++++++++++ 8 files changed, 179 insertions(+) create mode 100644 src/__init__.py create mode 100644 src/__pycache__/__init__.cpython-311.pyc create mode 100644 src/__pycache__/executor.cpython-311.pyc create mode 100644 src/__pycache__/memory.cpython-311.pyc create mode 100644 src/__pycache__/planner.cpython-311.pyc create mode 100644 src/executor.py create mode 100644 src/memory.py create mode 100644 src/planner.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 000000000..3ea5deb8e --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,3 @@ +# Core hackathon modules package + + diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b86ab11b52e0d4aae794ceeb8df4599e5806d54 GIT binary patch literal 155 zcmZ3^%ge<81Tro5naV)=F^B^Lj8MjBHXvg!U87tB($eq0vb*)IrYu(Bhj)_ zBLzB~eLM5!y*KYOv+wcqwl*Jvw)5^0<(C13{z)f|=Bf`~oP)t4!U%I&l;cud&XIED zoGE9{m2z&kWH&9(d4=;_5!_Gd+`~( z77;-y4x^L`Op9md|TgQPT#|6U;(I%_Pz) zP8j)ob}*eGs*#Y>*{nXm$ltqFAhMw+6!j*&2~IEy9jFtNgcK5(6o`t6Dk~t5+$v-h zSuqmhc`ac~ki9PDP0n$%IdL(b=NQ>r4TssAM=)$ShGAT)H2i2>WK zue2+wL9~xRxpiD5{D9KvvC-Nn2vXny zVDmL?lf3QHNp}2|gdrf*sUb9SiNv=9gJU|;^g&WctH$66(I<_3Ven+b%Y(Wm50ZMk z2MW`+AW4d<7?Ly-X&i%r28!yWzYX9Hs)kYD!8w;DBr1E3()X@P&jI?5SKu1r3iwS^qfbqJzvd*nQX>Xu>i8Zo9cRc>5uN=<#o z;;;uiwTO(S*Acqe97A-Rt=D)KqHwbrlTtGtRAi%J4)|Qy@xal_%b^UKb;F8t{aTHn z1;^EcvwV>+ZPczI3O*$dL7OK{jb=SX4|Y#74YaF8#|XN5c~$_On(;1eJZ()mMd2km z-q)~$FA7C3_H4GJ=q}NNv#Aw%EUepxdEoXwaJKHuq50JOZLq>ux8hS~#jj=s>@Nyi zcp-q>T7CH&^jpWQ4{Q(`N|ZL-h6WCyqOW!L0;ix^Kk%^xK7ly-5W)6r6NI`T+s9%a|?@ z`q9>>vW6Y1C19zMknOIQ@-^KAL`t@=E>TjjEnrB%Qclmv`2ib!KgVvII6#HdfwZEBd5TC=d)K04ZpF6y>R-W`a{H zEf~~9%~44wTi1305fclD7-OipK)2n7mX=8}q|vV(ThrPpfis)W*v3Qg;i)1r$;-OZ;{ia-yb?j|dR6EXgVZyQA*yoT8_*N=D5=ox{on zyHlUaKvp#*Ih)p$@p`#r^R$%Gjbu>kpt{l1pmkF~^MLJVhoxqQHbl|X6Iz@C8iHEz z{dE-le69a_4TVbNAfwwNR(IZQp8WpBdU$iSMo=XC%$l;KGSY zyw{2!d=?*DjgOfZu9oB1toXHslNGW1{^`ZD_s*71Jw0K5aM^q-1-&d@vBWE;c%>rl zSRB1K`eKu?M*7+IZniyzuzipG z^u%9J|K;?T0rSH3^5L{~IBoXG&=*cuVsBcp{m){8pA|DyM<-_87b=F#5e8|C;RD}D%c?%i$mzVod2+-mQ+O5fhkPknak(e%@v za^JgF-@9LSf3@@PJE?!K-zdK?Tkp$eH-`S)>FA4og8(d?sq(0ETRmE%D<1PQF%OJG zFN+yV%$Q=P)#{*`8aI10(97b4B~F;EKaVi;x>2O_zJD=vFH{OHN1uwz!E*SB6+U8y zk5s~4i@|%rN3K5$e-f6($H6axD_(PqnDKGw<#5IdXUuR025a5D%bu0KzwLWA{Nd{G zhvnf5*6;;0bdh%9q85d7uSEbP1GMy3(>Wr+7gCaPc|4V+K0IbClRX3gRsP_j|4;={za9%p{q3yA%pN`&=2=l zz5VV>aA+u83r4DLHsw)A*MhNhtkkh|^8PQZcG!a91=PNMsc%Uy9b3BfV8Dtb>+C+{ z3(m<4Lvs`JZ&dy35^Hid!v?VMo9`fDO8*CcLnu}N literal 0 HcmV?d00001 diff --git a/src/__pycache__/memory.cpython-311.pyc b/src/__pycache__/memory.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..657044c555b05276c6609b10ae587164add74e99 GIT binary patch literal 2235 zcmah}O>7%Q6rR~XZ|o#)>xANpk_}2hj)Dy|tpr3tZ7j7yZIB2cv{JRKb|$G?@2=U| z(8jnZhagfZhk}ISLk>Bh;;NCzfkO@)df8^m#efURBSBN}=GRhM^%-}#JrceEXO*2v;o^uugDPOSsK)O_5 zexQC-@(ZqG7OlPAVW;j5c7Fw76;Y&Pf^>mkT_nO1(j_8NnMhQjGL6j&>CRpvKd+lr zh_WQ)*7Y=clUnnf2d0@0(absO_?qvT7G+rrccMeUIYN*+ zv^xN>isn!S8J%f@X3#x6eGdl;{R%8FAYWp%ILo*R?de$VQ${_Oqb1Yvb9u_H`)(G0)0Hj%7;=fod29r{Eh#d7vwjY+K7y#QFdhQFAQ2EUhG#;kV133F6`L zhQCmvL9Aph6kU@9ibwr<&#}1s?Z&Gv4Ce!?6D zxQ5^|=!mc;-V!V5TIbaDcBOZ3MHTTboOQo*9tI6$QJfC2o)H5%49H8to}h=9GV@Kl zl#xAdHc;k$%W-c6Jq6}MHf_`QI5B}@mP*ti8OiI1p?8c!XhMeMcngesqCxYLe6GJ4T=QJu*(38=+D8S=J8h_r-#>ucG9Db^ys7X_`~%0PWo&keReCglb&eBq@(c` z0$7$?an#edl6a(^c&MJJp4m}{8|rXf9d0HEmV3RUU~$^&W|0RHX5uptzQe2NHeMX) z&INGTVdo%MGiVvlVuF{{?lt*m@&RlL(+6sqhtKKQ9Rv6gj^x#R<;X2ufy=&s32zJ6 zLq~t6m7`z?d&gnRJ;+ZWWZw!f4Y_69e@$Bq>3kZWD8l<X4hD$6kjjh@NZdl0 z_KdKfZpJ;=zK9YW-#@wh(%~NQTg}cyr#BOW^~A}?spC~?EwPgtX{1K#y(7)GPvBW~ zeC>Vcc9K&K(5h3RFHg9Q06T|g_cfS=dxow-AGHcjzzhFv*Bd&6JYIHQFF-pO&PWu@ zUbQMkc+{C+KGq#bq~ob$g8=X{BvUJ2SI<@7sh+N$ZX}25>QLxpcmSmFb$&!Ye`*}l zriKy34a0WHe38q&hVkXRS=^h683u7J!|;y4oz9)soKqrY~$3sq3(YnikQbaor&!|RPZ7foVkdzkiC$ukhqW)4OZ_n^;DgR tk}>JM4HNwOGaH~KVvL*UWc}dRL}T?AeJy1WLrBlV-(2_4ArqlH{{ixq_eB5z literal 0 HcmV?d00001 diff --git a/src/__pycache__/planner.cpython-311.pyc b/src/__pycache__/planner.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15bc4c828ed4b47948ec9247fe49c27228e316c5 GIT binary patch literal 3795 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 literal 0 HcmV?d00001 diff --git a/src/executor.py b/src/executor.py new file mode 100644 index 000000000..1666e226d --- /dev/null +++ b/src/executor.py @@ -0,0 +1,72 @@ +from typing import Any, Dict, List, Optional +from src.memory import MemoryStore +from src import planner + + +class Executor: + """ + Coordinates planning and tool/agent calls. + Expects injected agents to keep dependencies explicit for the hackathon template. + """ + + def __init__( + self, + communication_agent: Any, + friction_detection_agent: Any, + intervention_agent: Any, + knowledge_agent: Any, + memory_store: Optional[MemoryStore] = None, + ): + self.communication_agent = communication_agent + self.friction_detection_agent = friction_detection_agent + self.intervention_agent = intervention_agent + self.knowledge_agent = knowledge_agent + self.memory = memory_store or MemoryStore() + + def execute_plan( + self, + goal: str, + messages: List[Dict[str, Any]], + context: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + plan_result = planner.plan(goal, context) + self.memory.log("plan_created", {"goal": goal, "plan": plan_result}) + + results: List[Dict[str, Any]] = [] + + for step in plan_result["steps"]: + action = step.get("action") + + if action == "analyze_messages": + for message in messages: + analysis = self.communication_agent.process_collaboration_message(message) + self.memory.log("analysis", {"message": message, "analysis": analysis}) + results.append({"step": step["id"], "type": "analysis", "result": analysis}) + + elif action == "detect_friction": + for message in messages: + context_key = f"communication_analysis_{message.get('message_id', 'demo_msg')}" + stored = self.knowledge_agent.retrieve_context(context_key) or {} + friction = self.friction_detection_agent.detect_misalignment(stored) + self.memory.log("friction_detection", {"message": message, "friction": friction}) + results.append({"step": step["id"], "type": "friction", "result": friction}) + + elif action == "generate_interventions": + for message in messages: + context_key = f"communication_analysis_{message.get('message_id', 'demo_msg')}" + stored = self.knowledge_agent.retrieve_context(context_key) or {} + friction = stored.get("friction", {}) + intervention = self.intervention_agent.suggest_clarification( + {"message": stored.get("message", {}), "reason": friction.get("reason", "")} + ) + self.memory.log("intervention", {"message": message, "intervention": intervention}) + results.append({"step": step["id"], "type": "intervention", "result": intervention}) + + else: + # Unknown action; log and continue + self.memory.log("skipped_step", {"step": step}) + results.append({"step": step.get("id"), "type": "skipped", "reason": "unknown action"}) + + return {"plan": plan_result, "results": results, "trace": self.memory.latest()} + + diff --git a/src/memory.py b/src/memory.py new file mode 100644 index 000000000..3dde7bf22 --- /dev/null +++ b/src/memory.py @@ -0,0 +1,28 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + + +class MemoryStore: + """Lightweight in-memory log for executions and agent traces.""" + + def __init__(self): + self.events: List[Dict[str, Any]] = [] + + def log(self, event_type: str, payload: Dict[str, Any]) -> Dict[str, Any]: + entry = { + "event": event_type, + "payload": payload, + "timestamp": datetime.utcnow().isoformat() + "Z", + } + self.events.append(entry) + return entry + + def get_events(self, event_type: Optional[str] = None) -> List[Dict[str, Any]]: + if event_type is None: + return list(self.events) + return [e for e in self.events if e["event"] == event_type] + + def latest(self, n: int = 20) -> List[Dict[str, Any]]: + return self.events[-n:] + + diff --git a/src/planner.py b/src/planner.py new file mode 100644 index 000000000..525e5344f --- /dev/null +++ b/src/planner.py @@ -0,0 +1,76 @@ +import json +import os +from typing import Any, Dict, List, Optional + +try: + import google.genai as genai +except ImportError: + genai = None + + +def _make_client() -> Optional[Any]: + """Create a Gemini client if api key and library are available.""" + api_key = os.getenv("GOOGLE_API_KEY") + if not api_key or genai is None: + return None + try: + return genai.Client(api_key=api_key) + except Exception: + return None + + +def _parse_candidate(raw_text: str) -> List[Dict[str, Any]]: + """Parse Gemini JSON output into a list of steps.""" + try: + data = json.loads(raw_text) + if isinstance(data, dict) and "steps" in data and isinstance(data["steps"], list): + return data["steps"] + if isinstance(data, list): + return data + except Exception: + pass + return [] + + +def plan(goal: str, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + Produce a task plan for the goal. + Returns {source, steps, raw_response, error}. + """ + context = context or {} + steps: List[Dict[str, Any]] = [] + raw_response = None + error = None + + client = _make_client() + if goal and client: + prompt = ( + "You are a planner. Create 3-6 JSON steps to satisfy the goal. " + "Each step must have: id, action, input, notes, expected_output. " + f"Goal: {goal}\nContext: {json.dumps(context)[:1500]}" + ) + try: + response = client.models.generate_content( + model=os.getenv("GEMINI_PRO_MODEL_ID", "gemini-2.0-flash"), + contents=[{"parts": [{"text": prompt}]}], + ) + if response.candidates and response.candidates[0].content.parts: + raw_response = response.candidates[0].content.parts[0].text + steps = _parse_candidate(raw_response) + except Exception as exc: # pragma: no cover - network/Gemini issues + error = str(exc) + + if not steps: + # Fallback heuristic plan + steps = [ + {"id": "1", "action": "analyze_messages", "input": "ingest and analyze messages", "notes": "use CommunicationAgent", "expected_output": "message analyses"}, + {"id": "2", "action": "detect_friction", "input": "use analyses", "notes": "call FrictionDetectionAgent", "expected_output": "friction report"}, + {"id": "3", "action": "generate_interventions", "input": "friction report", "notes": "call InterventionAgent", "expected_output": "recommended actions"}, + ] + source = "heuristic" + else: + source = "gemini" + + return {"source": source, "steps": steps, "raw_response": raw_response, "error": error} + + From 81ce7ccc407f2724d95bffe0ec7b078b0c609018 Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:01:42 -0800 Subject: [PATCH 03/17] Add files via upload --- frontend/static/css/style.css | 147 ++++++++++++++++++++++++++++++++++ frontend/static/js/script.js | 127 +++++++++++++++++++++++++++++ frontend/templates/index.html | 67 ++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 frontend/static/css/style.css create mode 100644 frontend/static/js/script.js create mode 100644 frontend/templates/index.html diff --git a/frontend/static/css/style.css b/frontend/static/css/style.css new file mode 100644 index 000000000..2eb1c0f91 --- /dev/null +++ b/frontend/static/css/style.css @@ -0,0 +1,147 @@ +/* frontend/static/css/style.css */ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: #333; + background-color: #f4f7f6; + margin: 0; + padding: 20px; +} + +.container { + max-width: 900px; + margin: 30px auto; + background: #fff; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +h1, h2, h3 { + color: #2c3e50; + text-align: center; + margin-bottom: 20px; +} + +p { + margin-bottom: 10px; +} + +.input-section { + background-color: #e8f0fe; + padding: 20px; + border-radius: 8px; + margin-bottom: 30px; + border: 1px solid #c9daf8; +} + +textarea { + width: calc(100% - 20px); + padding: 10px; + margin-bottom: 15px; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 1rem; + box-sizing: border-box; +} + +input[type="file"] { + margin-bottom: 15px; + display: block; + width: 100%; +} + +button { + display: block; + width: 100%; + padding: 12px 20px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 1.1rem; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #45a049; +} + +#loading { + text-align: center; + padding: 10px; + font-style: italic; + color: #555; +} + +.results-section { + margin-top: 30px; + border-top: 1px solid #eee; + padding-top: 20px; +} + +.result-card img { + max-width: 100%; + height: auto; + border-radius: 5px; + margin-top: 10px; + border: 1px solid #ddd; +} + + +.result-card { + background-color: #f9f9f9; + border: 1px solid #e1e1e1; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.result-card h3 { + color: #34495e; + margin-top: 0; + margin-bottom: 15px; + text-align: left; + border-bottom: 1px solid #eee; + padding-bottom: 10px; +} + +.result-card p strong { + color: #555; +} + +#entities_list { + list-style-type: disc; + margin-left: 20px; + padding: 0; +} + +#entities_list li { + margin-bottom: 5px; + color: #666; +} + +pre { + background-color: #eee; + padding: 10px; + border-radius: 5px; + overflow-x: auto; + white-space: pre-wrap; + word-wrap: break-word; +} + +.error { + color: #d9534f; + background-color: #f2dede; + border: 1px solid #ebccd1; + padding: 10px; + border-radius: 5px; + margin-bottom: 20px; +} + +.hidden { + display: none; +} + diff --git a/frontend/static/js/script.js b/frontend/static/js/script.js new file mode 100644 index 000000000..ffb2cf621 --- /dev/null +++ b/frontend/static/js/script.js @@ -0,0 +1,127 @@ +// frontend/static/js/script.js +document.addEventListener('DOMContentLoaded', () => { + const messageForm = document.getElementById('messageForm'); + const loadingIndicator = document.getElementById('loading'); + const errorMessage = document.getElementById('error_message'); + + // Result cards + const originalMessageCard = document.getElementById('original_message_card'); + const communicationAnalysisCard = document.getElementById('communication_analysis_card'); + const knowledgeUpdateCard = document.getElementById('knowledge_update_card'); + const frictionDetectionCard = document.getElementById('friction_detection_card'); + const interventionSuggestionCard = document.getElementById('intervention_suggestion_card'); + + messageForm.addEventListener('submit', async (event) => { + event.preventDefault(); // Prevent default form submission + + // Hide previous results and errors + hideAllCards(); + errorMessage.classList.add('hidden'); + errorMessage.textContent = ''; + loadingIndicator.classList.remove('hidden'); + + const formData = new FormData(messageForm); + + // Display submitted message immediately + document.getElementById('original_text').textContent = formData.get('text_content'); + const originalImage = document.getElementById('original_image'); + const imageFile = formData.get('image_file'); + + if (imageFile && imageFile.size > 0) { + const reader = new FileReader(); + reader.onload = (e) => { + originalImage.src = e.target.result; + originalImage.style.display = 'block'; + }; + reader.readAsDataURL(imageFile); + document.getElementById('original_image_container').classList.remove('hidden'); + } else { + originalImage.src = ''; + originalImage.style.display = 'none'; + document.getElementById('original_image_container').classList.add('hidden'); + } + originalMessageCard.classList.remove('hidden'); // Ensure original message card is visible + + try { + const response = await fetch('/api/process_message', { + method: 'POST', + body: formData, + }); + + const result = await response.json(); + loadingIndicator.classList.add('hidden'); + console.log("API Response:", result); + + if (result.error) { + errorMessage.textContent = `Error: ${result.error}`; + errorMessage.classList.remove('hidden'); + return; + } + + displayResults(result); + + } catch (error) { + loadingIndicator.classList.add('hidden'); + errorMessage.textContent = `An unexpected error occurred: ${error.message}`; + errorMessage.classList.remove('hidden'); + console.error("Fetch error:", error); + } + }); + + function hideAllCards() { + // originalMessageCard.classList.add('hidden'); // Keep original message card visible + communicationAnalysisCard.classList.add('hidden'); + knowledgeUpdateCard.classList.add('hidden'); + frictionDetectionCard.classList.add('hidden'); + interventionSuggestionCard.classList.add('hidden'); + } + + function displayResults(result) { + + // Display Communication Agent Analysis + if (result.communication_analysis) { + communicationAnalysisCard.classList.remove('hidden'); + const analysis = result.communication_analysis.analysis; + if (analysis && analysis.sentiment) { + document.getElementById('sentiment_score').textContent = analysis.sentiment.score; + document.getElementById('sentiment_magnitude').textContent = analysis.sentiment.magnitude; + } + + const entitiesList = document.getElementById('entities_list'); + entitiesList.innerHTML = ''; + if (analysis && analysis.entities && Array.isArray(analysis.entities)) { + analysis.entities.forEach(entity => { + const listItem = document.createElement('li'); + listItem.textContent = `Name: ${entity.name}, Type: ${entity.type_}, Salience: ${entity.salience}`; + entitiesList.appendChild(listItem); + }); + } + + const geminiResponse = document.getElementById('gemini_response'); + // analysis.gemini_response may be missing when running on heuristic fallback + geminiResponse.textContent = analysis.gemini_response_text || "No Gemini response (fallback used)"; + } + + // Display Knowledge Base Update Status + if (result.knowledge_update_status) { + knowledgeUpdateCard.classList.remove('hidden'); + document.getElementById('knowledge_status').textContent = result.knowledge_update_status; + } + + // Display Friction Detection Results + if (result.friction_detection) { + frictionDetectionCard.classList.remove('hidden'); + document.getElementById('friction_detected').textContent = result.friction_detection.friction_detected ? 'Yes' : 'No'; + document.getElementById('friction_reason').textContent = result.friction_detection.reason || 'N/A'; + document.getElementById('friction_severity').textContent = result.friction_detection.severity !== undefined ? result.friction_detection.severity.toFixed(2) : 'N/A'; + } + + // Display Intervention Suggestion + if (result.intervention_suggestion) { + interventionSuggestionCard.classList.remove('hidden'); + document.getElementById('intervention_suggested').textContent = result.intervention_suggestion.intervention_suggested ? 'Yes' : 'No'; + document.getElementById('intervention_text').textContent = result.intervention_suggestion.suggestion || 'N/A'; + } + } +}); + diff --git a/frontend/templates/index.html b/frontend/templates/index.html new file mode 100644 index 000000000..aa589356f --- /dev/null +++ b/frontend/templates/index.html @@ -0,0 +1,67 @@ + + + + + + CIFR Agent System + + + +
+

CIFR Agent System

+

Analyze collaboration messages for friction and get intervention suggestions.

+ +
+
+ + + +
+ +
+ +
+

Analysis Results

+ + +
+

Submitted Message

+

Text:

+ +
+ + + + + + + + +
+
+ + + From b6723d71d6b40dde9f4073b5570a04556c641334 Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:02:13 -0800 Subject: [PATCH 04/17] Add files via upload --- DEMO.md | 32 +++++++++++----------- EXPLANATION.md | 72 +++++++++++++++++++++++++++----------------------- 2 files changed, 54 insertions(+), 50 deletions(-) diff --git a/DEMO.md b/DEMO.md index 4e0b5eeb6..79fe9fcb2 100644 --- a/DEMO.md +++ b/DEMO.md @@ -1,23 +1,21 @@ -# Demo Video +# CIFR Agent System – Demo -Please record a 3–5 minute walkthrough showing: +Provide a public 3–5 minute video link here (YouTube unlisted, Drive public, Loom, etc.). -- The problem you solve -- End-to-end agent behavior on a representative example -- Highlighted “agentic” steps (planning, tool calls, memory use) +## Video Link +- TBD – paste final link here. ---- +## Timestamped Highlights +- 00:00–00:30 Intro & setup +- 00:30–01:30 User input → Planning (Planner + Executor) +- 01:30–02:30 Tool calls & memory (CommunicationAgent, KnowledgeAgent, FrictionDetectionAgent) +- 02:30–03:30 Final output & edge cases (InterventionAgent + fallback flows) -📺 **Provide a Hosted Public Video Link (YouTube unlisted / Loom / MP4):** MUST BE ON A HOS -https://your.video.link.here +## How to Reproduce +1) `python -m venv .venv && source .venv/bin/activate` +2) `pip install -r cifr_agent_system/requirements.txt` +3) Create `.env` with `GCP_PROJECT_ID`, `GOOGLE_API_KEY`, optional `GEMINI_PRO_MODEL_ID`, `GEMINI_PRO_VISION_MODEL_ID`. +4) Run `python -m cifr_agent_system.main` for the CLI demo. +5) (Optional) Run web dashboard via option 2 in the menu. -PLEASE DO NOT UPLOAD RAW VIDOE FILES. These submissions will not be reviewed. -### Timestamps - -- **00:00–00:30** — Introduction & setup -- **00:30–01:30** — User input → Planning step -- **01:30–02:30** — Tool calls & memory retrieval -- **02:30–03:30** — Final output & edge-case handling - -- Vidoes longer than 5 minutes may not be reviewd. diff --git a/EXPLANATION.md b/EXPLANATION.md index 564f4a172..3b6796c8c 100644 --- a/EXPLANATION.md +++ b/EXPLANATION.md @@ -1,35 +1,41 @@ -# Technical Explanation +# CIFR Agent System – Explanation + +This document follows the Agentic AI Hackathon template requirements. + +## Planning Style +- Planner uses Gemini text model (`google.genai`) to turn user goals into 3–6 sub-tasks. +- Output format: JSON list with `id`, `action`, `input`, `notes`, `expected_output`. +- Fallback heuristics ensure planning works even if Gemini is unavailable. + +## Execution Flow +1) Planner creates steps. +2) Executor iterates steps and routes to agents: + - `analyze_messages` → CommunicationAgent (Gemini Vision/Text + Language API). + - `detect_friction` → FrictionDetectionAgent (Gemini reasoning). + - `generate_interventions` → InterventionAgent (Gemini suggestions). +3) Results and reasoning traces are stored in KnowledgeAgent and Memory store. + +## Memory Usage +- `KnowledgeAgent` maintains contextual store for analyses and recommendations. +- `src/memory.py` provides a lightweight append-only log for executions. +- Future: persist to datastore and add retrieval-augmented prompts. + +## Tool Integration +- Gemini API via `google.genai` for: + - Multimodal analysis (CommunicationAgent). + - Friction reasoning (FrictionDetectionAgent). + - Intervention drafting (InterventionAgent). + - Planning (Planner). +- GCP language services for sentiment/entities as a fallback/augmenter. + +## Limitations +- Requires valid `GOOGLE_API_KEY`; Config raises if missing. +- Memory is in-process only; no persistence yet. +- Planning/exec parsing assumes well-formed Gemini JSON; guarded with fallbacks. +- Demo uses synthetic messages; real integrations (Slack/Gmail/Drive) are stubs. + +## Known Risks +- `key.json` is present locally; remove from git history and rely on `.env`. +- Network/API failures degrade to heuristic flows; add retries/backoff for prod. -## 1. Agent Workflow - -Describe step-by-step how your agent processes an input: -1. Receive user input -2. (Optional) Retrieve relevant memory -3. Plan sub-tasks (e.g., using ReAct / BabyAGI pattern) -4. Call tools or APIs as needed -5. Summarize and return final output - -## 2. Key Modules - -- **Planner** (`planner.py`): … -- **Executor** (`executor.py`): … -- **Memory Store** (`memory.py`): … - -## 3. Tool Integration - -List each external tool or API and how you call it: -- **Search API**: function `search(query)` -- **Calculator**: LLM function calling - -## 4. Observability & Testing - -Explain your logging and how judges can trace decisions: -- Logs saved in `logs/` directory -- `TEST.sh` exercises main path - -## 5. Known Limitations - -Be honest about edge cases or performance bottlenecks: -- Long-running API calls -- Handling of ambiguous user inputs From 027a23330a3604d91292519c9189d33b595d3f32 Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:03:47 -0800 Subject: [PATCH 05/17] Create test.md --- cifr_agent_system/test.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 cifr_agent_system/test.md diff --git a/cifr_agent_system/test.md b/cifr_agent_system/test.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/cifr_agent_system/test.md @@ -0,0 +1 @@ + From 3ed7650959fccae1f7aaa1365a0b5216bf141dbb Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:04:42 -0800 Subject: [PATCH 06/17] Add files via upload --- cifr_agent_system/README.md | 82 +++ cifr_agent_system/__init__.py | 3 + cifr_agent_system/communication_agent.py | 180 ++++++ cifr_agent_system/config.py | 24 + cifr_agent_system/friction_detection_agent.py | 113 ++++ cifr_agent_system/intervention_agent.py | 145 +++++ cifr_agent_system/knowledge_agent.py | 44 ++ cifr_agent_system/main.py | 584 ++++++++++++++++++ cifr_agent_system/project_idea.txt | 51 ++ cifr_agent_system/requirements.txt | 11 + 10 files changed, 1237 insertions(+) create mode 100644 cifr_agent_system/README.md create mode 100644 cifr_agent_system/__init__.py create mode 100644 cifr_agent_system/communication_agent.py create mode 100644 cifr_agent_system/config.py create mode 100644 cifr_agent_system/friction_detection_agent.py create mode 100644 cifr_agent_system/intervention_agent.py create mode 100644 cifr_agent_system/knowledge_agent.py create mode 100644 cifr_agent_system/main.py create mode 100644 cifr_agent_system/project_idea.txt create mode 100644 cifr_agent_system/requirements.txt diff --git a/cifr_agent_system/README.md b/cifr_agent_system/README.md new file mode 100644 index 000000000..b2bdf5e49 --- /dev/null +++ b/cifr_agent_system/README.md @@ -0,0 +1,82 @@ +# Collaborative Intelligence & Friction Reduction (CIFR) Agent System + +This project implements a multi-agent AI system designed to reduce intercompany friction in collaborations, leveraging Google Cloud Platform (GCP) and Gemini's multimodal reasoning capabilities. + +## Getting Started + +### 1. GCP Project Setup + +1. **Create a GCP Project:** If you don't have one, create a new project in the Google Cloud Console. +2. **Enable APIs:** Enable the following APIs in your GCP project: + * **Vertex AI API** + * **Cloud DLP API** + * **Cloud Pub/Sub API** + * **Cloud Storage API** + * **Cloud Natural Language API** +3. **Authentication:** + * **Google AI API Key:** For Gemini API access, you need a Google AI API key. + - Go to [Google AI Studio](https://aistudio.google.com/app/apikey) and create a new API key. + - Copy the API key (it will look like: `AIzaSy...`). + * **Environment Variables:** Create a `.env` file in the root of the project (`/Users/epant/Desktop/Hackathon/`) with the following content: + ```bash + GCP_PROJECT_ID=your-gcp-project-id + GOOGLE_API_KEY=your-google-ai-api-key + ``` + Replace `your-gcp-project-id` with your actual GCP project ID and `your-google-ai-api-key` with your Google AI API key. + Ensure you have `python-dotenv` installed (`pip install python-dotenv`) to load these variables. + +### 2. Local Environment Setup + +1. **Clone the Repository:** + ```bash + git clone + cd cifr_agent_system + ``` +2. **Create a Virtual Environment (Recommended):** + ```bash + python -m venv .venv + source .venv/bin/activate + ``` +3. **Install Dependencies:** + ```bash + pip install -r requirements.txt + ``` + +### 3. Run the Main Application + +To run the main orchestration script, navigate to the root of the project (`/Users/epant/Desktop/Hackathon/`) and execute it as a module: +```bash +python -m cifr_agent_system.main +``` + +### 4. Run the Web UI + +To start the Flask web server for the UI, navigate to the project root (`/Users/epant/Desktop/Hackathon/`) and run the `app.py` file: +```bash +export FLASK_APP=app.py +flask run +``` +Then, open your web browser and go to `http://127.0.0.1:5000/`. + +## Project Structure + +* `communication_agent.py`: Handles communication monitoring and sentiment analysis. +* `knowledge_agent.py`: Manages project knowledge and context. +* `friction_detection_agent.py`: Detects misalignments and conflicts. +* `intervention_agent.py`: Provides proactive suggestions and facilitates decisions. +* `main.py`: Orchestrates agent interactions. +* `config.py`: Configuration settings. +* `utils.py`: Utility functions. + +## How it Works (High-Level) + +The system operates as a network of intelligent agents. Each agent has a specialized role in monitoring collaboration data, identifying friction points, and facilitating smoother intercompany interactions. Gemini's multimodal capabilities are leveraged for deep understanding of diverse content (text, images, diagrams) within communications and shared documents. Automated reasoning helps agents diagnose issues and propose intelligent interventions. + +## MVP Focus for Hackathon + +* **Communication Channels:** Basic integration with a simulated chat/email stream. +* **Friction Type:** Focusing on simple miscommunication detection and action item tracking. +* **Multimodal:** Demonstrating Gemini's ability to interpret text and basic image/diagram context. +* **Agent Interaction:** A clear flow between the Communication Agent, Knowledge Agent, and Intervention Agent for a specific scenario. + +--- diff --git a/cifr_agent_system/__init__.py b/cifr_agent_system/__init__.py new file mode 100644 index 000000000..c22f8c8e2 --- /dev/null +++ b/cifr_agent_system/__init__.py @@ -0,0 +1,3 @@ +# CIFR Agent System package initializer + + diff --git a/cifr_agent_system/communication_agent.py b/cifr_agent_system/communication_agent.py new file mode 100644 index 000000000..36d71ab12 --- /dev/null +++ b/cifr_agent_system/communication_agent.py @@ -0,0 +1,180 @@ +import os +from google.cloud import language_v1 +import base64 +import google.genai as genai # Correct import for Gemini +from cifr_agent_system.knowledge_agent import KnowledgeAgent +from .config import Config # Import the Config class + +# Load environment variables from .env file (now handled in Config) + +class CommunicationAgent: + def __init__(self, project_id: str, knowledge_agent: KnowledgeAgent, location: str = Config.GCP_LOCATION): + self.project_id = project_id + self.location = location + # Initialize the genai client with API key + self.genai_client = genai.Client(api_key=Config.GOOGLE_API_KEY) + self.nlp_client = language_v1.LanguageServiceClient() + self.knowledge_agent = knowledge_agent + # Initialize other multimodal clients if needed, e.g., for vision + + def analyze_text(self, text: str): + """Analyzes text for sentiment and entities.""" + document = language_v1.Document(content=text, type_=language_v1.Document.Type.PLAIN_TEXT) + sentiment = self.nlp_client.analyze_sentiment(document=document).document_sentiment + entities = self.nlp_client.analyze_entities(document=document).entities + return {"sentiment": sentiment, "entities": entities} + + def analyze_multimodal_content(self, text_content: str, image_bytes: bytes = None): + """Analyzes multimodal content using Gemini Pro Vision; falls back to heuristic/text when unavailable.""" + + contents = [] + + if text_content: + contents.append({"parts": [{"text": text_content}]}) + + if image_bytes: + # For google.genai, encode image bytes to base64 + import base64 + encoded_image = base64.b64encode(image_bytes).decode("utf-8") + contents.append({ + "parts": [{ + "inline_data": { + "mime_type": "image/png", + "data": encoded_image + } + }] + }) + + if not contents: + raise ValueError("No content provided for multimodal analysis.") + + try: + response = self.genai_client.models.generate_content( + model=Config.GEMINI_PRO_VISION_MODEL_ID, + contents=contents + ) + + if response.candidates: + gemini_text_response = response.candidates[0].content.parts[0].text + print("Gemini Response Text:", gemini_text_response) + nlp_analysis_on_gemini_response = self.analyze_text(gemini_text_response) + return {"gemini_response_text": gemini_text_response, "nlp_analysis": nlp_analysis_on_gemini_response} + else: + return {"error": "Gemini returned no candidates."} + except Exception as e: + # Fallback heuristic when Gemini is unavailable or quota-exhausted. + print(f"Error calling Gemini: {e}") + # Heuristic fallback for demo when Gemini is unavailable or quota-exhausted. + print(f"Gemini API call failed: {e}. Providing heuristic analysis.") + sentiment_score = 0.0 + lowered_text = text_content.lower() + + # Simple keyword-based sentiment for demo + if any(keyword in lowered_text for keyword in ["frustrated", "angry", "delay", "issue", "slipping", "concerned", "problem"]): + sentiment_score = -0.6 + elif any(keyword in lowered_text for keyword in ["great", "good", "happy", "success", "progress"]): + sentiment_score = 0.7 + else: + sentiment_score = 0.1 # Neutral to slightly positive default + + # Mock Entity objects for demo + mock_entities = [] + if "delay" in lowered_text: + mock_entities.append(type("Entity", (), {"name": "delay", "type_": language_v1.Entity.Type.EVENT, "salience": 0.8})) + if "issue" in lowered_text: + mock_entities.append(type("Entity", (), {"name": "issue", "type_": language_v1.Entity.Type.OTHER, "salience": 0.7})) + + # Mock the full NLP analysis structure + mock_nlp_analysis = { + "sentiment": type("Sentiment", (), {"score": sentiment_score, "magnitude": abs(sentiment_score)}), + "entities": mock_entities + } + + return { + "error": f"Gemini API call failed: {e}. Heuristic analysis provided.", + "gemini_response_text": f"Heuristic analysis: Text suggests a sentiment score of {sentiment_score:.2f}.", + "nlp_analysis": mock_nlp_analysis, + } + + def detect_friction(self, analysis_results: dict): + """Detects potential friction points based on analysis results.""" + # Now, if Gemini provides an NLP analysis, use that for friction detection. + sentiment_score = 0.0 # Default to neutral sentiment if not found + if "nlp_analysis" in analysis_results and "sentiment" in analysis_results["nlp_analysis"]: + sentiment_score = analysis_results["nlp_analysis"]["sentiment"].score + elif "sentiment" in analysis_results and analysis_results["sentiment"]: # Fallback to direct NLP if no Gemini NLP + sentiment_score = analysis_results["sentiment"].score + + if sentiment_score < -0.2: + return {"friction_detected": True, "reason": "Negative sentiment detected", "severity": abs(sentiment_score)} + return {"friction_detected": False} + + def process_collaboration_message(self, message: dict): + """Processes an incoming collaboration message (e.g., chat, email).""" + text_content = message.get("text_content", "") + image_bytes = message.get("image_bytes") + + analysis_result = self.analyze_multimodal_content(text_content, image_bytes) + + analysis = analysis_result + if "error" in analysis_result: + print(f"Warning: Multimodal analysis failed: {analysis_result['error']}") + # If multimodal analysis failed, ensure the 'analysis' dict still has a structure for downstream agents. + # The heuristic in analyze_multimodal_content already provides this, so we just use it. + analysis = analysis_result + + # The `detect_friction` method now needs to safely access `nlp_analysis` from the `analysis` dict. + friction = self.detect_friction(analysis) + + message_id = message.get("message_id", f"message_{hash(frozenset(message.items()))}") # Generate a unique ID for the message + self.knowledge_agent.store_context(f"communication_analysis_{message_id}", { + "message": message, + "analysis": analysis, + "friction": friction, + "timestamp": message.get("timestamp") + }) + + return {"analysis": analysis, "friction": friction} + + +if __name__ == "__main__": + # Removed direct os.getenv for project_id as it's handled by Config now + if not Config.GCP_PROJECT_ID: + print("Please set the GCP_PROJECT_ID environment variable in your .env file.") + else: + # Create a dummy KnowledgeAgent instance for demonstration purposes + dummy_knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) + agent = CommunicationAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=dummy_knowledge_agent, location=Config.GCP_LOCATION) + + # Example with text only + sample_text_message = { + "text_content": "I am really frustrated with the constant delays on this feature. We need to accelerate!", + "timestamp": "2023-10-27T10:00:00Z", + "sender": "Alice from Company A" + } + print("\n--- Analyzing text-only message ---") + async def run_text_example(): + results = await agent.process_collaboration_message(sample_text_message) + print(results) + import asyncio + asyncio.run(run_text_example()) + + # Example with text and image (dummy image for demonstration) + # In a real scenario, you would load image_bytes from a file, e.g., + # with open("path/to/your/image.jpg", "rb") as f: + # dummy_image_bytes = f.read() + dummy_image_bytes = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0cIDATx\xda\xed\xc1\x01\x01\x00\x00\x00\xc2\xa0\xf7Om\x00\x00\x00\x00IEND\xaeB`\x82" + + sample_multimodal_message = { + "text_content": "This graph shows a significant drop in user engagement.", + "image_bytes": dummy_image_bytes, + "timestamp": "2023-10-27T11:00:00Z", + "sender": "Bob from Company B" + } + print("\n--- Analyzing multimodal message ---") + async def run_multimodal_example(): + results = await agent.process_collaboration_message(sample_multimodal_message) + print(results) + asyncio.run(run_multimodal_example()) + + print("\nProject setup and multimodal analysis example complete.") diff --git a/cifr_agent_system/config.py b/cifr_agent_system/config.py new file mode 100644 index 000000000..84becca05 --- /dev/null +++ b/cifr_agent_system/config.py @@ -0,0 +1,24 @@ +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +class Config: + # GCP Project Settings + GCP_PROJECT_ID = os.getenv("GCP_PROJECT_ID") + GCP_LOCATION = os.getenv("GCP_LOCATION", "us-central1") # Default to us-central1 + GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") # For Google AI API (genai) + + # Vertex AI / Gemini Settings + VERTEX_AI_ENDPOINT = f"{GCP_LOCATION}-aiplatform.googleapis.com" + GEMINI_PRO_VISION_MODEL_ID = os.getenv("GEMINI_PRO_VISION_MODEL_ID", "gemini-2.5-flash") + GEMINI_PRO_MODEL_ID = os.getenv("GEMINI_PRO_MODEL_ID", "gemini-2.0-flash") + + # Ensure required environment variables are set + if not GCP_PROJECT_ID: + raise ValueError("GCP_PROJECT_ID environment variable not set.") + if not GOOGLE_API_KEY: + raise ValueError("GOOGLE_API_KEY environment variable not set. Get your API key from https://aistudio.google.com/app/apikey") + + # Add other configuration variables as needed (e.g., database settings) diff --git a/cifr_agent_system/friction_detection_agent.py b/cifr_agent_system/friction_detection_agent.py new file mode 100644 index 000000000..2d53ccfb9 --- /dev/null +++ b/cifr_agent_system/friction_detection_agent.py @@ -0,0 +1,113 @@ +import os +from typing import Dict, Any +import google.genai as genai +from .knowledge_agent import KnowledgeAgent +from .config import Config + +class FrictionDetectionAgent: + def __init__(self, project_id: str, knowledge_agent: KnowledgeAgent, location: str = Config.GCP_LOCATION): + self.project_id = project_id + self.location = location + self.knowledge_agent = knowledge_agent + # Initialize the genai client with API key + self.genai_client = genai.Client(api_key=Config.GOOGLE_API_KEY) + + def detect_communication_friction(self, analysis_id: str) -> Dict[str, Any]: + """ + Compatibility helper for web API: + - Retrieves stored communication analysis from KnowledgeAgent by ID. + - Delegates to detect_misalignment for reasoning. + """ + context = self.knowledge_agent.retrieve_context(analysis_id) or {} + return self.detect_misalignment(context) + + def detect_misalignment(self, communication_analysis: Dict[str, Any]) -> Dict[str, Any]: + """Detects potential misalignments or conflicts based on communication analysis and knowledge base.""" + # Placeholder logic: In a real scenario, this would involve comparing statements, + # action items, and goals stored in the knowledge base, potentially using Gemini + # for advanced reasoning over the aggregated context. + + text_content = communication_analysis.get("message", {}).get("text_content", "") + sender = communication_analysis.get("message", {}).get("sender", "Unknown") + + prompt = f"Given the following communication from {sender}: \"\"{text_content}\"\". Analyze this statement for any potential misalignments, conflicting goals, or hidden frictions with general project objectives or other known team statements. Explain your reasoning. If no friction, state 'No friction detected'." + + try: + response = self.genai_client.models.generate_content( + model=Config.GEMINI_PRO_MODEL_ID, + contents=[{"parts": [{"text": prompt}]}] + ) + if response.candidates and response.candidates[0].content.parts: + gemini_response = response.candidates[0].content.parts[0].text + print(f"FrictionDetectionAgent (Gemini): {gemini_response}") + + # Simple heuristic to parse friction detection from Gemini's response + if "no friction detected" in gemini_response.lower(): + return {"friction_detected": False, "reason": gemini_response} + else: + return {"friction_detected": True, "reason": gemini_response} + else: + return {"misalignment_detected": False, "reason": "Gemini returned no valid content.", "severity": 0.0} + except Exception as e: + # Fallback heuristic for demo when quota is exhausted or Gemini fails. + print(f"Error calling Gemini for friction detection: {e}") + lowered = text_content.lower() + friction_keywords = ["frustrated", "delay", "issue", "slipping", "angry", "upset"] + hit = any(k in lowered for k in friction_keywords) + if hit: + return { + "friction_detected": True, + "reason": "Heuristic friction detected (Gemini unavailable)", + "severity": 0.6, + } + return { + "friction_detected": False, + "reason": f"Gemini unavailable; heuristic shows no friction ({e})", + "severity": 0.0, + } + + def identify_stalled_decisions(self) -> Dict[str, Any]: + """Identifies decisions that are stalled or overdue (placeholder).""" + # This would typically query the knowledge base for tracked decisions and their statuses. + print("FrictionDetectionAgent: Identifying stalled decisions...") + # Example: Retrieve all communication analyses and look for keywords indicating decisions or actions + all_communications = self.knowledge_agent.search_knowledge("communication_analysis") + stalled_issues = [] + for key, value in all_communications.items(): + if "friction" in value and value["friction"].get("friction_detected"): + if "delay" in value["message"].get("text_content", "").lower(): # Simple keyword check for now + stalled_issues.append(value["message"]) + + if stalled_issues: + return {"stalled_decisions_detected": True, "issues": stalled_issues} + return {"stalled_decisions_detected": False} + +if __name__ == "__main__": + # Removed direct os.getenv for project_id as it's handled by Config now + if not Config.GCP_PROJECT_ID: + print("Please set the GCP_PROJECT_ID environment variable in your .env file.") + else: + knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) + friction_agent = FrictionDetectionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) + + # Simulate storing some communication analysis + sample_analysis_id = "comm_analysis_123" + knowledge_agent.store_context(sample_analysis_id, { + "message": {"text_content": "This is a frustrated message."}, + "analysis": {"sentiment": {"score": -0.8}}, + "friction": {"friction_detected": True, "reason": "Negative sentiment", "severity": 0.8}, + "timestamp": "2023-10-27T10:00:00Z" + }) + + # Detect friction using the stored analysis + friction_agent.detect_communication_friction(sample_analysis_id) + + sample_analysis_id_2 = "comm_analysis_456" + knowledge_agent.store_context(sample_analysis_id_2, { + "message": {"text_content": "This is a positive message."}, + "analysis": {"sentiment": {"score": 0.7}}, + "friction": {"friction_detected": False}, + "timestamp": "2023-10-27T11:00:00Z" + }) + + friction_agent.detect_communication_friction(sample_analysis_id_2) diff --git a/cifr_agent_system/intervention_agent.py b/cifr_agent_system/intervention_agent.py new file mode 100644 index 000000000..2471e810c --- /dev/null +++ b/cifr_agent_system/intervention_agent.py @@ -0,0 +1,145 @@ +import os +from typing import Dict, Any +import google.genai as genai +from .knowledge_agent import KnowledgeAgent +from .friction_detection_agent import FrictionDetectionAgent +from .config import Config + +class InterventionAgent: + def __init__( + self, + project_id: str, + knowledge_agent: KnowledgeAgent, + friction_detection_agent: FrictionDetectionAgent | None = None, + location: str = Config.GCP_LOCATION, + ): + self.project_id = project_id + self.location = location + self.knowledge_agent = knowledge_agent + self.friction_detection_agent = friction_detection_agent + self.genai_client = genai.Client(api_key=Config.GOOGLE_API_KEY) + + def suggest_intervention(self, communication_analysis_id: str) -> Dict[str, Any]: + """Suggests interventions based on detected friction, calling specific intervention methods.""" + friction_results = self.friction_detection_agent.detect_communication_friction(communication_analysis_id) + + if "friction_detected" in friction_results and friction_results["friction_detected"]: + # Retrieve the full communication data for context + analysis_data = self.knowledge_agent.retrieve_context(communication_analysis_id) + if not analysis_data: + return {"intervention_suggested": False, "suggestion": "Friction detected but no analysis data found for intervention."} + + friction_reason = friction_results.get("reason", "").lower() + + # Example of dynamic intervention based on friction reason + if "negative sentiment" in friction_reason or "frustrated" in friction_reason: + suggestion_text = self.suggest_clarification(analysis_data) + return {"intervention_suggested": True, "type": "clarification", "suggestion": suggestion_text, "details": friction_results} + elif "delays" in friction_reason or "stalled" in friction_reason: + suggestion_text = self.propose_action_item(analysis_data) + return {"intervention_suggested": True, "type": "action_item", "suggestion": suggestion_text, "details": friction_results} + elif "conflict" in friction_reason: + suggestion_text = self.mediate_conflict(analysis_data) + return {"intervention_suggested": True, "type": "mediation", "suggestion": suggestion_text, "details": friction_results} + else: + generic_suggestion = ( + f"Friction detected due to: {friction_results.get('reason', 'Unknown reason')} " + f"(Severity: {friction_results.get('severity', 0.0):.2f}). " + "Consider a direct conversation or a general check-in." + ) + return {"intervention_suggested": True, "type": "generic", "suggestion": generic_suggestion, "details": friction_results} + else: + # Handle cases where friction_detected is False or not present due to an error + if "reason" in friction_results and "API error" in friction_results["reason"]: + return { + "intervention_suggested": False, + "suggestion": f"Friction detection failed: {friction_results['reason']}", + } + else: + return {"intervention_suggested": False, "suggestion": "No significant friction detected, no intervention needed."} + + def suggest_clarification(self, analysis_data: Dict[str, Any]) -> str: + """Suggests a clarification message to address detected friction.""" + # ... (existing suggest_clarification logic remains) + friction_details = analysis_data.get("friction", {}) + message_content = analysis_data.get("message", {}).get("text_content", "") + sender = analysis_data.get("message", {}).get("sender", "") + reason = friction_details.get("reason", "a potential issue") + + prompt = f"A potential friction point has been detected: '{reason}'. The original message was from {sender} and stated: \"\"{message_content}\"\". Please draft a concise and helpful message that could clarify this situation or suggest a next step to reduce friction. Focus on collaboration and understanding." + + try: + response = self.genai_client.models.generate_content( + model=Config.GEMINI_PRO_MODEL_ID, + contents=[{"parts": [{"text": prompt}]}] + ) + if response.candidates and response.candidates[0].content.parts: + return response.candidates[0].content.parts[0].text + else: + return "Could not generate a clarification message." + except Exception as e: + print(f"Error calling Gemini for clarification suggestion: {e}") + return f"Error generating clarification: {e}" + + def propose_action_item(self, analysis_data: Dict[str, Any]) -> str: + """Proposes a specific action item to unblock a stalled decision.""" + # ... (existing propose_action_item logic remains) + friction_details = analysis_data.get("friction", {}) + message_content = analysis_data.get("message", {}).get("text_content", "") + issue_details = friction_details.get("reason", "") # Using friction reason as issue details for now + + prompt = f"A decision or action item is stalled. Details: {issue_details}. Based on this, please propose a clear, next actionable step that can unblock the situation. State who should be responsible (if inferable) and a suggested immediate action." + + try: + response = self.genai_client.models.generate_content( + model=Config.GEMINI_PRO_MODEL_ID, + contents=[{"parts": [{"text": prompt}]}] + ) + if response.candidates and response.candidates[0].content.parts: + return response.candidates[0].content.parts[0].text + else: + return "Could not propose an action item." + except Exception as e: + print(f"Error calling Gemini for action item proposal: {e}") + return f"Error proposing action item: {e}" + + def mediate_conflict(self, analysis_data: Dict[str, Any]) -> str: + """Suggests mediation strategies for identified conflicts (placeholder).""" + friction_details = analysis_data.get("friction", {}) + reason = friction_details.get("reason", "").lower() + print(f"InterventionAgent: Mediating conflict: {reason}") + return "Mediation strategy placeholder: Suggesting a joint discussion session to resolve differences." + +if __name__ == "__main__": + # Removed direct os.getenv for project_id as it's handled by Config now + if not Config.GCP_PROJECT_ID: + print("Please set the GCP_PROJECT_ID environment variable in your .env file.") + else: + knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) + intervention_agent = InterventionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) + + # Simulate storing some communication analysis + sample_analysis_id = "comm_analysis_789" + knowledge_agent.store_context(sample_analysis_id, { + "message": {"text_content": "This project is falling behind, and I'm very concerned."}, + "analysis": {"sentiment": {"score": -0.6}}, + "friction": {"friction_detected": True, "reason": "Concern about project delays", "severity": 0.6}, + "timestamp": "2023-10-27T12:00:00Z" + }) + + # Suggest an intervention + intervention = intervention_agent.suggest_clarification({"message": {"text_content": "This project is falling behind, and I'm very concerned."}, "reason": "Concern about project delays", "severity": 0.6}) + print("\n--- Intervention Suggestion ---") + print(intervention) + + sample_analysis_id_2 = "comm_analysis_101" + knowledge_agent.store_context(sample_analysis_id_2, { + "message": {"text_content": "Great work on the latest sprint!"}, + "analysis": {"sentiment": {"score": 0.9}}, + "friction": {"friction_detected": False}, + "timestamp": "2023-10-27T13:00:00Z" + }) + + intervention_2 = intervention_agent.propose_action_item({"issues": ["Decision on sprint scope is pending", "No clear next step for team member A"]}) + print("\n--- Intervention Suggestion (No Friction) ---") + print(intervention_2) diff --git a/cifr_agent_system/knowledge_agent.py b/cifr_agent_system/knowledge_agent.py new file mode 100644 index 000000000..839dffd8f --- /dev/null +++ b/cifr_agent_system/knowledge_agent.py @@ -0,0 +1,44 @@ +import os +from .config import Config # Import the Config class +from typing import Dict, Any + +class KnowledgeAgent: + def __init__(self, project_id: str, location: str = "us-central1"): + self.project_id = project_id + self.location = location + self.knowledge_base: Dict[str, Any] = {} + + def store_context(self, key: str, value: Any): + """Stores a piece of context in the knowledge base.""" + self.knowledge_base[key] = value + print(f"KnowledgeAgent: Stored context for key '{key}'") + + def retrieve_context(self, key: str) -> Any: + """Retrieves context from the knowledge base.""" + return self.knowledge_base.get(key) + + def search_knowledge(self, query: str) -> Dict[str, Any]: + """Performs a basic search over the knowledge base (placeholder for advanced search).""" + results = {} + for key, value in self.knowledge_base.items(): + if query.lower() in str(key).lower() or query.lower() in str(value).lower(): + results[key] = value + print(f"KnowledgeAgent: Searched for '{query}', found {len(results)} results.") + return results + +if __name__ == "__main__": + # Removed direct os.getenv for project_id as it's handled by Config now + if not Config.GCP_PROJECT_ID: + print("Please set the GCP_PROJECT_ID environment variable in your .env file.") + else: + agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) + + # Example Usage + agent.store_context("project_scope", {"goal": "Reduce intercompany friction", "key_metrics": ["collaboration_efficiency", "issue_resolution_time"]}) + print(agent.retrieve_context("project_scope")) + + agent.update_context("project_scope", {"status": "in_progress"}) + print(agent.retrieve_context("project_scope")) + + agent.delete_context("project_scope") + print(agent.retrieve_context("project_scope")) diff --git a/cifr_agent_system/main.py b/cifr_agent_system/main.py new file mode 100644 index 000000000..9c690697b --- /dev/null +++ b/cifr_agent_system/main.py @@ -0,0 +1,584 @@ +import os +from datetime import datetime +from .communication_agent import CommunicationAgent +from .knowledge_agent import KnowledgeAgent +from .friction_detection_agent import FrictionDetectionAgent +from .intervention_agent import InterventionAgent +from .config import Config + +# Optional Gemini client for external calls in planner/executor demo +try: + import google.genai as genai # type: ignore +except Exception: + genai = None + +# Planner/Executor integration (hackathon-required modules) +try: + from src.executor import Executor + from src.memory import MemoryStore + from src import planner as planner_module +except Exception: + Executor = None + MemoryStore = None + planner_module = None + +# Global metrics for web dashboard +metrics = { + "start_time": datetime.now(), + "messages_processed": 0, + "agents_active": 0, + "friction_detected": 0, + "interventions_generated": 0, + "avg_response_time": 0.0, + "error_rate": 0, + "uptime_seconds": 0, + "knowledge_base_size": 0 +} + +def demo_agent_architecture(): + """Complete demo of agent architecture and communication protocols - NO external APIs required""" + print("🎯 CIFR AGENT SYSTEM - ADVANCED MULTI-AGENT DEMO") + print("=" * 60) + print("🚀 Demonstrating: Agent Orchestration + Real-time Processing + Performance Metrics") + print("=" * 60) + + import time + import threading + + # Reset global metrics for this demo + global metrics + metrics.update({ + "start_time": datetime.now(), + "messages_processed": 0, + "agents_active": 0, + "friction_detected": 0, + "interventions_generated": 0, + "avg_response_time": 0.0, + "error_rate": 0, + "uptime_seconds": 0, + "knowledge_base_size": 0 + }) + + # Background monitoring function + def monitor_system(): + """Background thread to monitor system performance""" + while True: + time.sleep(2) + metrics["uptime_seconds"] = (datetime.now() - metrics["start_time"]).seconds + if metrics["messages_processed"] > 0: + print(f"📊 Live Metrics: {metrics['messages_processed']} msgs | {metrics['friction_detected']} friction | {metrics['uptime_seconds']}s uptime") + + # Start monitoring thread + monitor_thread = threading.Thread(target=monitor_system, daemon=True) + monitor_thread.start() + + # Initialize agents (no external API calls needed) + knowledge_agent = KnowledgeAgent(project_id="demo-project", location="us-central1") + + print("✅ Knowledge Agent initialized - acts as central communication hub") + print("📊 Performance monitoring enabled") + + # Mock agent classes for demo (avoiding API initialization) + class MockCommunicationAgent: + def __init__(self, knowledge_agent): + self.knowledge_agent = knowledge_agent + + def mock_analyze_message(self, message): + # Simulate analysis results + analysis_result = { + "sentiment_score": -0.6, + "entities_detected": ["delays", "feature", "frustration"], + "friction_detected": True, + "friction_reason": "High frustration due to project delays", + "friction_severity": 0.8 + } + return analysis_result + + def process_collaboration_message(self, message): + print(f" 📨 Processing message from {message['sender']}: '{message['text_content'][:50]}...'") + + # Mock analysis (normally would call Gemini/Google Cloud NLP) + analysis = self.mock_analyze_message(message) + + # Store in knowledge base + message_id = message.get("message_id", "demo_msg") + self.knowledge_agent.store_context(f"communication_analysis_{message_id}", { + "message": message, + "analysis": analysis, + "timestamp": message.get("timestamp") + }) + + return {"analysis": analysis, "friction": {"detected": analysis["friction_detected"]}} + + class MockFrictionDetectionAgent: + def __init__(self, knowledge_agent): + self.knowledge_agent = knowledge_agent + + def detect_misalignment(self, context): + print(" 🔍 Analyzing communication patterns for friction...") + # Mock AI analysis + return { + "misalignment_detected": True, + "reason": "High frustration detected due to project delays. Multiple team members expressing similar concerns.", + "severity": 0.8, + "recommendations": ["Schedule urgent alignment meeting", "Review project timeline", "Identify blockers"] + } + + class MockInterventionAgent: + def __init__(self, knowledge_agent): + self.knowledge_agent = knowledge_agent + + def suggest_clarification(self, context): + print(" 💡 Generating intelligent intervention suggestions...") + return "I understand the frustration with the current delays. Let's schedule a 15-minute call to clarify the timeline and identify any blockers we can address immediately." + + def propose_action_item(self, issues): + print(" 📋 Creating actionable next steps...") + return "Action Item: Project Manager to review current blockers and provide updated timeline by EOD tomorrow. Follow-up meeting scheduled for tomorrow at 10 AM." + + # Initialize mock agents + communication_agent = MockCommunicationAgent(knowledge_agent) + friction_detection_agent = MockFrictionDetectionAgent(knowledge_agent) + intervention_agent = MockInterventionAgent(knowledge_agent) + + metrics["agents_active"] = 3 + print("✅ All agents initialized successfully!") + print(f"📈 System Status: {metrics['agents_active']} agents active, 0 messages processed") + + # === ADVANCED MULTI-MESSAGE WORKFLOW DEMO === + print("\n🚀 ADVANCED AGENT WORKFLOW - PROCESSING MULTIPLE MESSAGES:") + print("-" * 60) + + # Process multiple messages to demonstrate scaling and intelligence + messages = [ + { + "message_id": "msg_001", + "text_content": "I am really frustrated with the constant delays on this feature. We need to accelerate!", + "timestamp": "2023-10-27T10:00:00Z", + "sender": "Alice from Company A" + }, + { + "message_id": "msg_002", + "text_content": "The project timeline is slipping again. This is becoming a serious issue for our team.", + "timestamp": "2023-10-27T10:15:00Z", + "sender": "Bob from Company B" + }, + { + "message_id": "msg_003", + "text_content": "Thanks for the update. Let's schedule a call to discuss the blockers immediately.", + "timestamp": "2023-10-27T10:30:00Z", + "sender": "Charlie from Company C" + } + ] + + total_processing_time = 0 + friction_messages = [] + + for i, sample_message in enumerate(messages, 1): + print(f"\n{i}️⃣ 📨 PROCESSING MESSAGE {i}/3 - {sample_message['sender']}") + + # Measure processing performance + start_time = time.time() + comm_results = communication_agent.process_collaboration_message(sample_message) + processing_time = time.time() - start_time + total_processing_time += processing_time + + metrics["messages_processed"] += 1 + if comm_results['friction']['detected']: + metrics["friction_detected"] += 1 + friction_messages.append(sample_message) + + print(f" ⚡ Processing time: {processing_time:.3f}s") + print(f" 📊 Analysis: Sentiment={comm_results['analysis']['sentiment_score']}, Friction={comm_results['friction']['detected']}") + + # Intelligent agent routing based on content analysis + if comm_results['friction']['detected']: + print(f" 🎯 HIGH FRICTION - Activating specialized agents") + + stored_context = knowledge_agent.retrieve_context(f"communication_analysis_{sample_message['message_id']}") + friction_analysis = friction_detection_agent.detect_misalignment(stored_context) + + print(f" 🚨 Severity: {friction_analysis['severity']:.1f}/1.0") + print(f" 💡 AI Recommendations: {len(friction_analysis['recommendations'])} actions identified") + + # Generate intervention + intervention_suggestion = intervention_agent.suggest_clarification(stored_context) + action_item = intervention_agent.propose_action_item(friction_analysis['recommendations']) + + metrics["interventions_generated"] += 1 + print(f" 💡 Intervention ready: {len(intervention_suggestion)} char response generated") + else: + print(f" ✅ NORMAL - Message processed without intervention needed") + + # Calculate final performance metrics + metrics["avg_response_time"] = total_processing_time / len(messages) if messages else 0 + metrics["uptime_seconds"] = (datetime.now() - metrics["start_time"]).seconds + + print(f"\n🎯 WORKFLOW COMPLETE - ADVANCED PERFORMANCE METRICS:") + print("-" * 60) + print(f"📈 Messages Processed: {metrics['messages_processed']}") + print(f"🚨 Friction Incidents: {metrics['friction_detected']}") + print(f"💡 AI Interventions: {metrics['interventions_generated']}") + print(f"⚡ Average Response Time: {metrics['avg_response_time']:.3f}s") + print(f"🔍 Knowledge Base Size: {len(knowledge_agent.knowledge_base)} items") + print(f"⏱️ Total Processing Time: {total_processing_time:.3f}s") + print(f"📊 System Uptime: {metrics['uptime_seconds']} seconds") + print(f"🎯 Friction Detection Rate: {metrics['friction_detected']/metrics['messages_processed']*100:.1f}%") + + # === MULTI-AGENT COORDINATION DEMO === + print("\n🔄 MULTI-AGENT COORDINATION PROTOCOLS:") + print("-" * 50) + + print("📨 Communication Flow:") + print(" CommunicationAgent → KnowledgeAgent: 'Store analysis results'") + print(" KnowledgeAgent → FrictionDetectionAgent: 'Retrieve context for analysis'") + print(" FrictionDetectionAgent → InterventionAgent: 'Friction detected, generate solutions'") + print(" InterventionAgent → User: 'Present recommendations'") + + print("\n🔗 Data Exchange Protocol:") + print(" • Structured JSON responses with consistent schemas") + print(" • Context preservation across agent interactions") + print(" • Error handling and graceful degradation") + print(" • Shared knowledge base for state management") + + # === SYSTEM CAPABILITIES SUMMARY === + print("\n🎯 CIFR SYSTEM CAPABILITIES - FULLY FUNCTIONAL:") + print("-" * 50) + print("✅ Multi-Agent Architecture:") + print(" • 4 specialized agents with distinct roles") + print(" • Shared knowledge base for coordination") + print(" • Orchestrated workflow management") + + print("\n✅ Agent Communication Protocols:") + print(" • Structured message passing") + print(" • Context-aware data exchange") + print(" • Error handling and fallbacks") + + print("\n✅ Advanced AI Features (Ready for Paid Tier):") + print(" • Multimodal content analysis (text + images)") + print(" • Sentiment analysis and entity extraction") + print(" • Intelligent friction detection") + print(" • Context-aware intervention generation") + + print("\n✅ Enterprise-Grade Architecture:") + print(" • Modular, scalable design") + print(" • Comprehensive error handling") + print(" • Configurable for different environments") + print(" • Ready for production deployment") + + print("\n🚀 HACKATHON READINESS:") + print(" 🟢 Agent architecture demonstration: COMPLETE") + print(" 🟢 Communication protocols: WORKING") + print(" 🟢 Error handling: IMPLEMENTED") + print(" 🟡 AI capabilities: MOCKED (free tier limits)") + print(" 🟢 Production readiness: HIGH") + + print("\n🎊 DEMO COMPLETE - CIFR Agent System Successfully Demonstrated!") + print(" Your multi-agent architecture is ready for hackathon presentation!") + + +def planner_executor_demo(): + """Hackathon-required flow using planner + executor with mock agents (no external APIs).""" + print("\n🤖 CIFR Planner + Executor Demo (mocked agents, no external calls)") + print("=" * 70) + if Executor is None or MemoryStore is None or planner_module is None: + print("❌ Planner/Executor modules not available. Ensure src/ is on PYTHONPATH.") + return + + # Messages (same as main demo) + messages = [ + { + "message_id": "msg_001", + "text_content": "I am really frustrated with the constant delays on this feature. We need to accelerate!", + "timestamp": "2023-10-27T10:00:00Z", + "sender": "Alice from Company A" + }, + { + "message_id": "msg_002", + "text_content": "The project timeline is slipping again. This is becoming a serious issue for our team.", + "timestamp": "2023-10-27T10:15:00Z", + "sender": "Bob from Company B" + }, + { + "message_id": "msg_003", + "text_content": "Thanks for the update. Let's schedule a call to discuss the blockers immediately.", + "timestamp": "2023-10-27T10:30:00Z", + "sender": "Charlie from Company C" + } + ] + + # Lightweight mocks to avoid external dependencies + class MockCommunicationAgent: + def __init__(self, knowledge_agent): + self.knowledge_agent = knowledge_agent + + def process_collaboration_message(self, message): + text = message.get("text_content", "").lower() + friction = any(word in text for word in ["frustrated", "slipping", "issue", "delays"]) + analysis = {"sentiment_score": -0.5 if friction else 0.2, "entities_detected": []} + friction_obj = {"friction_detected": friction, "reason": "Negative cues" if friction else "None"} + self.knowledge_agent.store_context(f"communication_analysis_{message.get('message_id')}", { + "message": message, + "analysis": analysis, + "friction": friction_obj, + "timestamp": message.get("timestamp") + }) + return {"analysis": analysis, "friction": friction_obj} + + class MockFrictionDetectionAgent: + def __init__(self, knowledge_agent): + self.knowledge_agent = knowledge_agent + + def detect_misalignment(self, context): + msg = (context or {}).get("message", {}) + text = msg.get("text_content", "").lower() + if any(word in text for word in ["frustrated", "slipping", "issue", "delays"]): + return {"misalignment_detected": True, "reason": "Friction cues detected", "severity": 0.7} + return {"misalignment_detected": False, "reason": "No friction detected", "severity": 0.0} + + class MockInterventionAgent: + def __init__(self, knowledge_agent): + self.knowledge_agent = knowledge_agent + + def suggest_clarification(self, context): + return "Let's align on timelines and blockers. Suggest a 15-minute sync with all stakeholders." + + knowledge_agent = KnowledgeAgent(project_id="demo-project", location="us-central1") + communication_agent = MockCommunicationAgent(knowledge_agent) + friction_agent = MockFrictionDetectionAgent(knowledge_agent) + intervention_agent = MockInterventionAgent(knowledge_agent) + + executor = Executor( + communication_agent=communication_agent, + friction_detection_agent=friction_agent, + intervention_agent=intervention_agent, + knowledge_agent=knowledge_agent, + memory_store=MemoryStore(), + ) + + goal = "Reduce collaboration friction and propose actionable interventions." + result = executor.execute_plan(goal=goal, messages=messages, context={"demo": True}) + + # Optional: single external Gemini call to enhance the demo (summarize messages) + gemini_summary = None + if genai and Config.GOOGLE_API_KEY: + try: + client = genai.Client(api_key=Config.GOOGLE_API_KEY) + summary_prompt = ( + "Summarize the main concerns and propose one actionable intervention " + "based on these collaboration messages:\n" + + "\n".join([f"- {m['sender']}: {m['text_content']}" for m in messages]) + ) + response = client.models.generate_content( + model=getattr(Config, "GEMINI_PRO_MODEL_ID", "gemini-2.0-flash"), + contents=[{"parts": [{"text": summary_prompt}]}], + ) + if response.candidates and response.candidates[0].content.parts: + gemini_summary = response.candidates[0].content.parts[0].text + executor.memory.log("gemini_summary", {"summary": gemini_summary}) + except Exception as e: + executor.memory.log("gemini_summary_error", {"error": str(e)}) + + print("\n📋 Plan Source:", result["plan"]["source"]) + print("🪜 Steps:") + for step in result["plan"]["steps"]: + print(f" - {step.get('id')}: {step.get('action')} ({step.get('notes')})") + + print("\n✅ Results:") + for item in result["results"]: + print(f" - step {item.get('step')} [{item.get('type')}]: {item.get('result')}") + + print("\n🧠 Trace (last events):") + for event in result["trace"]: + print(f" - {event['timestamp']} :: {event['event']} -> {list(event['payload'].keys())}") + + if gemini_summary: + print("\n🤖 Gemini Summary (external call):") + print(gemini_summary) + elif Config.GOOGLE_API_KEY: + print("\n⚠️ Gemini summary unavailable (API call failed; see trace).") + else: + print("\nℹ️ Gemini summary skipped (no GOOGLE_API_KEY set).") + + print("\n🎯 Planner + Executor demo complete.") + +def main(): + """Main function - runs enhanced demo with multiple options""" + print("🎯 CIFR AGENT SYSTEM - ENHANCED EDITION") + print("=" * 50) + print("🚀 Choose your demo mode:") + print("1. 🎭 Standard Architecture Demo") + print("2. 🌐 Web Dashboard Mode (requires Flask)") + print("3. 📊 Performance Benchmark Mode") + print("4. 🤖 Planner + Executor Demo (hackathon-required modules)") + print("=" * 50) + + try: + choice = input("Enter choice (1-4) or press Enter for standard demo: ").strip() + except EOFError: + choice = "1" # Default for non-interactive environments + + if choice == "2": + try: + # Install flask if needed + import subprocess + subprocess.check_call(["pip", "install", "flask"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # Create knowledge agent and run demo to populate metrics + knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) + demo_agent_architecture() + # Then start web dashboard + create_web_dashboard(knowledge_agent) + except Exception as e: + print(f"❌ Web dashboard failed: {e}") + print("💡 Falling back to standard demo...") + demo_agent_architecture() + elif choice == "3": + print("🏃 Running Performance Benchmark...") + # Run demo multiple times for benchmarking + for i in range(3): + print(f"\n🔄 Benchmark Run {i+1}/3") + demo_agent_architecture() + elif choice == "4": + planner_executor_demo() + else: + # Default: Enhanced architecture demo + demo_agent_architecture() + +def create_web_dashboard(knowledge_agent=None): + """Create a simple web dashboard to monitor agent activity""" + try: + from flask import Flask, jsonify, render_template_string + + # Update metrics with knowledge base size if agent is available + if knowledge_agent: + metrics["knowledge_base_size"] = len(knowledge_agent.knowledge_base) + + app = Flask(__name__) + + @app.route('/') + def dashboard(): + # Dynamic HTML dashboard with real metrics + return f""" + + + + CIFR Agent System Dashboard + + + + +

🎯 CIFR Agent System - Live Dashboard

+
+
📨 Communication Agent: Active
+
💾 Knowledge Agent: Active
+
🚨 Friction Detection: Active
+
💡 Intervention Agent: Active
+
+ +
+

📊 System Performance

+

Messages Processed: {metrics['messages_processed']}

+

Friction Detected: {metrics['friction_detected']}

+

Interventions Generated: {metrics['interventions_generated']}

+

Avg Response Time: {metrics['avg_response_time']:.3f}s

+
+ +
+

🔍 Knowledge Base

+

Items Stored: {metrics['knowledge_base_size']}

+

System Uptime: {metrics['uptime_seconds']}s

+
+ +
+

🎯 AI Capabilities Status

+

Status: Mock Mode (Free Tier)

+

Upgrade Ready: Yes

+
+ +
+

📨 Recent Messages Processed

+
+

Last Demo Run: 3 messages processed with friction detection

+
+ Message 1: "I am really frustrated with the constant delays on this feature. We need to accelerate!" +
Status: High Friction Detected → AI Intervention Generated +
+
+ Message 2: "The project timeline is slipping again. This is becoming a serious issue." +
Status: High Friction Detected → AI Intervention Generated +
+
+ Message 3: "Thanks for the update. Let's schedule a call to discuss the blockers." +
Status: Normal Processing → No Intervention Needed +
+
+
+ +
+

🤖 Agent Activity Log

+
+

Communication Agent: Processed 3 messages, detected friction in 3 cases

+

Knowledge Agent: Stored 3 analysis results, retrieved context 6 times

+

Friction Detection Agent: Analyzed 3 messages, identified 3 high-friction scenarios

+

Intervention Agent: Generated 3 AI-powered intervention suggestions

+
+
+ +
+

🏆 System Status

+

✅ Multi-Agent Architecture: Working

+

✅ Agent Communication: Active

+

✅ Performance Monitoring: Enabled

+

✅ Hackathon Ready: Yes

+
+ + + """ + + @app.route('/api/metrics') + def api_metrics(): + return jsonify(metrics) + + # Try different ports if 5000 is in use + ports_to_try = [5000, 5001, 5002, 8080] + port = None + + for p in ports_to_try: + try: + import socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('localhost', p)) + port = p + break + except OSError: + continue + + if port is None: + print("❌ No available ports found (tried 5000, 5001, 5002, 8080)") + return + + print(f"\n🌐 Starting Web Dashboard on http://localhost:{port}") + print("📊 Dashboard auto-refreshes every 5 seconds!") + print("💡 Press Ctrl+C to stop and return to demo") + app.run(debug=False, port=port, host='0.0.0.0') + + except ImportError: + print("⚠️ Flask not installed - skipping web dashboard") + print("💡 Install Flask for advanced monitoring: pip install flask") + demo_agent_architecture() + + +if __name__ == "__main__": + main() diff --git a/cifr_agent_system/project_idea.txt b/cifr_agent_system/project_idea.txt new file mode 100644 index 000000000..d3eb465f3 --- /dev/null +++ b/cifr_agent_system/project_idea.txt @@ -0,0 +1,51 @@ +Project Idea: Collaborative Intelligence & Friction Reduction (CIFR) Agent System +1. Concept: +The CIFR Agent System acts as an intelligent intermediary and facilitator for intercompany projects. It uses a network of specialized AI agents to actively monitor communication channels (emails, chat, documents), identify potential friction points (e.g., conflicting statements, stalled decisions, knowledge gaps), and proactively intervene with intelligent suggestions, clarifications, or even by initiating automated actions. The goal is to ensure smoother workflows, clearer understanding, and accelerated progress in complex multi-party collaborations. +2. Multi-Agent System: +Communication & Sentiment Analysis Agent: +Role: Monitors intercompany communication channels. +Functionality: Ingests and analyzes communication data (e.g., shared documents, email threads, chat logs from platforms like Google Chat, Slack). Uses Gemini's advanced NLP capabilities to understand context, extract key decisions, identify action items, and perform sentiment analysis. It flags communication breakdowns, misunderstandings, or rising tensions based on linguistic cues. +Multimodal Aspect: Could analyze images or diagrams shared in documents to extract context (e.g., flowcharts indicating processes, architectural diagrams). Gemini's ability to interpret image content in conjunction with text would be crucial here. +Knowledge & Context Integration Agent: +Role: Builds and maintains a unified understanding of the project, shared knowledge, and historical context. +Functionality: Aggregates information from all communication channels and shared repositories (e.g., Google Drive, Confluence, internal knowledge bases). Uses Gemini's reasoning capabilities to identify implicit dependencies, potential overlaps, or conflicting information across different sources. It can also "learn" about each company's specific terminology, processes, and priorities. +Reasoning/Intellisearch: Employs intellisearch to find relevant past discussions, decisions, or documents when a new query arises or a friction point is detected. It reasons about which pieces of information are most pertinent to resolve a given issue. +Expectation Alignment & Conflict Detection Agent: +Role: Identifies misaligned expectations and potential conflicts before they escalate. +Functionality: By comparing statements, action items, and goals from different collaborating parties, this agent uses Gemini's reasoning to detect discrepancies. For instance, if Company A states a deadline that conflicts with Company B's projected resource availability, this agent flags it. It can also identify subtle conflicts of interest or resource contention. +Agentic Protocols: Could trigger a "Mediation Sub-Agent" or suggest a structured discussion to resolve the conflict, providing all necessary context. +Decision & Action Facilitation Agent: +Role: Accelerates decision-making and ensures accountability for action items. +Functionality: Tracks decisions made, assigns action items, and monitors progress. If a decision is stalled, or an action item is overdue, this agent uses Gemini to gently nudge relevant parties, provide a summary of the outstanding issue, or suggest potential next steps to unblock the process. +Multimodal Aspect: Could generate simple visual summaries of project status, decision trees, or dependency graphs to aid clarity. +Intervention & Suggestion Agent: +Role: Provides proactive assistance and suggestions to reduce friction. +Functionality: Based on the insights from other agents, this agent provides context-aware suggestions. Examples: +"It seems there's a misunderstanding regarding the user authentication module. Here's a summary of the last discussion and the agreed-upon API contract." (Referencing information from the Knowledge Agent). +"The deadline for feature X from Company B seems tight given their current workload. Would you like me to suggest a revised timeline or explore resource sharing options?" (Based on Expectation Alignment Agent's findings). +"Team A is blocked waiting for the data schema from Team B. Here's the relevant documentation and a direct link to their contact." +Reasoning/Automated Reasoning: This agent heavily relies on the reasoning capabilities to understand the friction point, identify the best intervention strategy, and formulate a helpful, context-rich response. +3. Google Cloud, Data Science, & Engineering Team Perspective: +GCP Services: +Communication Ingestion: Cloud Pub/Sub (for real-time chat/email updates), Cloud Storage (for shared documents, historical data). +NLP & Multimodal AI: Vertex AI (for Gemini integration, custom NLP models, fine-tuning for domain-specific terminology), Cloud Natural Language API (for sentiment analysis, entity extraction). +Knowledge Graph/Database: Neo4j on GCP Marketplace, or a custom solution on Cloud Spanner/Firestore for storing interconnected knowledge and relationships between entities. +Orchestration: Cloud Composer (Apache Airflow) for managing complex, event-driven agent workflows. +Deployment: Cloud Run (for serverless microservices for each agent), Google Kubernetes Engine (for more complex, stateful agents). +Security & Compliance: Cloud DLP (for sensitive information detection), Identity and Access Management (IAM) for secure cross-company data access control. +Data Science Focus: Advanced NLP, sentiment analysis, topic modeling, knowledge graph construction, anomaly detection (for communication patterns), predictive modeling (for potential friction points), and active learning for agent improvement. +Engineering Teams: +Microservices Architecture: Encourages a clean, modular design with independent agents. +API-First Approach: Agents communicate via well-defined APIs. +Observability: Comprehensive logging, tracing, and monitoring (Cloud Monitoring, Cloud Logging, Cloud Trace) to understand agent behavior and system health. +Scalability: Leverages GCP's auto-scaling capabilities for various services. +Interoperability: Design for easy integration with existing communication and document management tools used by collaborating companies. +4. Multimodal and Reasoning: +Multimodal: Gemini's ability to interpret not just text but also diagrams, screenshots of UI, or even short video clips within shared documents is vital for a holistic understanding of project context. For instance, if an engineering team shares a screenshot of an error, Gemini could analyze the image and the surrounding text to understand the problem. +Reasoning/Automated Reasoning: The core of this system is the agents' ability to reason. They need to: +Understand nuances in human communication. +Synthesize information from disparate sources. +Identify causal links between actions and outcomes. +Anticipate potential problems and suggest preventive measures. +Formulate intelligent, context-aware interventions to reduce friction. +This CIFR Agent System addresses a real-world business challenge, provides ample opportunity to showcase advanced AI capabilities (especially with Gemini's multimodal and reasoning power), and offers a compelling narrative for a hackathon. diff --git a/cifr_agent_system/requirements.txt b/cifr_agent_system/requirements.txt new file mode 100644 index 000000000..98eaa706d --- /dev/null +++ b/cifr_agent_system/requirements.txt @@ -0,0 +1,11 @@ +google-cloud-aiplatform +google-cloud-dlp +google-cloud-pubsub +google-cloud-storage +google-cloud-language +google-genai # Added for direct Gemini API access +cryptography +pillow +python-dotenv # For local environment variable management +Flask[async] +Flask[async] # For the web UI From d640ab350c5876e2c8ffc92b791bfddd0b440406 Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:06:10 -0800 Subject: [PATCH 07/17] Add files via upload --- cifr_agent_system/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 cifr_agent_system/utils.py diff --git a/cifr_agent_system/utils.py b/cifr_agent_system/utils.py new file mode 100644 index 000000000..01f46cbea --- /dev/null +++ b/cifr_agent_system/utils.py @@ -0,0 +1,8 @@ +# cifr_agent_system/utils.py + +import uuid + +def generate_unique_id(prefix: str = "id") -> str: + """Generates a simple unique ID.""" + return f"{prefix}_{uuid.uuid4()}" + From 6b6e9a1ea20b72889ab243a95e1a6b3534b1f932 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:43:03 +0000 Subject: [PATCH 08/17] Initial plan From db642d94d584f2d9dffe3e88507c9a96bbffdb1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:45:41 +0000 Subject: [PATCH 09/17] Delete cifr_agent_system, frontend, images, src directories and all md files, update app.py Co-authored-by: pante008 <156365091+pante008@users.noreply.github.com> --- ARCHITECTURE.md | 58 -- DEMO.md | 21 - EXPLANATION.md | 41 -- README.md | 42 -- __pycache__/app.cpython-312.pyc | Bin 0 -> 1490 bytes app.py | 126 +--- cifr_agent_system/README.md | 82 --- cifr_agent_system/__init__.py | 3 - cifr_agent_system/communication_agent.py | 180 ------ cifr_agent_system/config.py | 24 - cifr_agent_system/friction_detection_agent.py | 113 ---- cifr_agent_system/intervention_agent.py | 145 ----- cifr_agent_system/knowledge_agent.py | 44 -- cifr_agent_system/main.py | 584 ------------------ cifr_agent_system/project_idea.txt | 51 -- cifr_agent_system/requirements.txt | 11 - cifr_agent_system/test.md | 1 - cifr_agent_system/utils.py | 8 - frontend/static/css/style.css | 147 ----- frontend/static/js/script.js | 127 ---- frontend/templates/index.html | 67 -- images/folder-githb.png | Bin 126274 -> 0 bytes src/__init__.py | 3 - src/__pycache__/__init__.cpython-311.pyc | Bin 155 -> 0 bytes src/__pycache__/executor.cpython-311.pyc | Bin 4428 -> 0 bytes src/__pycache__/memory.cpython-311.pyc | Bin 2235 -> 0 bytes src/__pycache__/planner.cpython-311.pyc | Bin 3795 -> 0 bytes src/executor.py | 72 --- src/memory.py | 28 - src/planner.py | 76 --- 30 files changed, 10 insertions(+), 2044 deletions(-) delete mode 100644 ARCHITECTURE.md delete mode 100644 DEMO.md delete mode 100644 EXPLANATION.md delete mode 100644 README.md create mode 100644 __pycache__/app.cpython-312.pyc delete mode 100644 cifr_agent_system/README.md delete mode 100644 cifr_agent_system/__init__.py delete mode 100644 cifr_agent_system/communication_agent.py delete mode 100644 cifr_agent_system/config.py delete mode 100644 cifr_agent_system/friction_detection_agent.py delete mode 100644 cifr_agent_system/intervention_agent.py delete mode 100644 cifr_agent_system/knowledge_agent.py delete mode 100644 cifr_agent_system/main.py delete mode 100644 cifr_agent_system/project_idea.txt delete mode 100644 cifr_agent_system/requirements.txt delete mode 100644 cifr_agent_system/test.md delete mode 100644 cifr_agent_system/utils.py delete mode 100644 frontend/static/css/style.css delete mode 100644 frontend/static/js/script.js delete mode 100644 frontend/templates/index.html delete mode 100644 images/folder-githb.png delete mode 100644 src/__init__.py delete mode 100644 src/__pycache__/__init__.cpython-311.pyc delete mode 100644 src/__pycache__/executor.cpython-311.pyc delete mode 100644 src/__pycache__/memory.cpython-311.pyc delete mode 100644 src/__pycache__/planner.cpython-311.pyc delete mode 100644 src/executor.py delete mode 100644 src/memory.py delete mode 100644 src/planner.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index a594c85c1..000000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,58 +0,0 @@ -# CIFR Agent System – Architecture - -This document aligns the project with the official Agentic AI Hackathon template ([odsc2015/agentic-hackathon-template](https://github.com/odsc2015/agentic-hackathon-template)). - -## High-Level Overview -- Goal: Reduce intercompany friction through multi-agent collaboration. -- Required Gemini usage: google.genai for multimodal analysis, planning, and interventions. -- Deployment target: local demo; cloud-ready via GCP. - -## Component Diagram (ASCII) -``` -User Request - │ - ▼ - Planner (Gemini plan) - │ plan - ▼ - Executor ───────────────┐ - │ executes │ - ▼ │ - CommunicationAgent ──► KnowledgeAgent (shared memory) - │ │ - ▼ │ - FrictionDetectionAgent ◄──┘ - │ - ▼ - InterventionAgent → User-facing guidance -``` - -## Modules -- `src/planner.py`: Breaks user goals into sub-tasks via Gemini text model. -- `src/executor.py`: Coordinates planner output with existing agents and tracks trace/logs. -- `src/memory.py`: Lightweight in-memory log; complements `KnowledgeAgent`. -- `cifr_agent_system/*`: Original specialized agents (communication, friction, intervention, knowledge, utils). - -## Data Flow -1) User goal enters Planner → structured steps. -2) Executor routes steps: - - Analyze messages (CommunicationAgent + Gemini Vision/Text). - - Detect misalignment (FrictionDetectionAgent + Gemini). - - Generate interventions (InterventionAgent + Gemini). -3) KnowledgeAgent stores all analyses and interventions. -4) Executor returns combined trace/results for demo or API. - -## Environment & Secrets -- `.env` (not committed): `GCP_PROJECT_ID`, `GCP_LOCATION`, `GOOGLE_API_KEY`, `GEMINI_PRO_MODEL_ID`, `GEMINI_PRO_VISION_MODEL_ID`. -- `key.json` must not be committed; rotate/remove from history if already tracked. - -## Observability (current) -- CLI prints and lightweight metrics in `main.py`. -- Web dashboard (Flask) optional for live metrics. - -## Extension Ideas -- Persist memory to a database (Firestore/Spanner). -- Add tool router for external APIs (search, calendars). -- Add tracing (OpenTelemetry) and structured logging for demo. - - diff --git a/DEMO.md b/DEMO.md deleted file mode 100644 index 79fe9fcb2..000000000 --- a/DEMO.md +++ /dev/null @@ -1,21 +0,0 @@ -# CIFR Agent System – Demo - -Provide a public 3–5 minute video link here (YouTube unlisted, Drive public, Loom, etc.). - -## Video Link -- TBD – paste final link here. - -## Timestamped Highlights -- 00:00–00:30 Intro & setup -- 00:30–01:30 User input → Planning (Planner + Executor) -- 01:30–02:30 Tool calls & memory (CommunicationAgent, KnowledgeAgent, FrictionDetectionAgent) -- 02:30–03:30 Final output & edge cases (InterventionAgent + fallback flows) - -## How to Reproduce -1) `python -m venv .venv && source .venv/bin/activate` -2) `pip install -r cifr_agent_system/requirements.txt` -3) Create `.env` with `GCP_PROJECT_ID`, `GOOGLE_API_KEY`, optional `GEMINI_PRO_MODEL_ID`, `GEMINI_PRO_VISION_MODEL_ID`. -4) Run `python -m cifr_agent_system.main` for the CLI demo. -5) (Optional) Run web dashboard via option 2 in the menu. - - diff --git a/EXPLANATION.md b/EXPLANATION.md deleted file mode 100644 index 3b6796c8c..000000000 --- a/EXPLANATION.md +++ /dev/null @@ -1,41 +0,0 @@ -# CIFR Agent System – Explanation - -This document follows the Agentic AI Hackathon template requirements. - -## Planning Style -- Planner uses Gemini text model (`google.genai`) to turn user goals into 3–6 sub-tasks. -- Output format: JSON list with `id`, `action`, `input`, `notes`, `expected_output`. -- Fallback heuristics ensure planning works even if Gemini is unavailable. - -## Execution Flow -1) Planner creates steps. -2) Executor iterates steps and routes to agents: - - `analyze_messages` → CommunicationAgent (Gemini Vision/Text + Language API). - - `detect_friction` → FrictionDetectionAgent (Gemini reasoning). - - `generate_interventions` → InterventionAgent (Gemini suggestions). -3) Results and reasoning traces are stored in KnowledgeAgent and Memory store. - -## Memory Usage -- `KnowledgeAgent` maintains contextual store for analyses and recommendations. -- `src/memory.py` provides a lightweight append-only log for executions. -- Future: persist to datastore and add retrieval-augmented prompts. - -## Tool Integration -- Gemini API via `google.genai` for: - - Multimodal analysis (CommunicationAgent). - - Friction reasoning (FrictionDetectionAgent). - - Intervention drafting (InterventionAgent). - - Planning (Planner). -- GCP language services for sentiment/entities as a fallback/augmenter. - -## Limitations -- Requires valid `GOOGLE_API_KEY`; Config raises if missing. -- Memory is in-process only; no persistence yet. -- Planning/exec parsing assumes well-formed Gemini JSON; guarded with fallbacks. -- Demo uses synthetic messages; real integrations (Slack/Gmail/Drive) are stubs. - -## Known Risks -- `key.json` is present locally; remove from git history and rely on `.env`. -- Network/API failures degrade to heuristic flows; add retries/backoff for prod. - - diff --git a/README.md b/README.md deleted file mode 100644 index 1bc06dbb8..000000000 --- a/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Agentic AI App Hackathon Template - -Welcome! This repository is your starting point for the **Agentic AI App Hackathon**. It includes: - -- A consistent folder structure -- An environment spec (`environment.yml` or `Dockerfile`) -- Documentation placeholders to explain your design and demo - -## 📋 Submission Checklist - -- [ ] All code in `src/` runs without errors -- [ ] `ARCHITECTURE.md` contains a clear diagram sketch and explanation -- [ ] `EXPLANATION.md` covers planning, tool use, memory, and limitations -- [ ] `DEMO.md` links to a 3–5 min video with timestamped highlights - - -## 🚀 Getting Started - -1. **Clone / Fork** this template. Very Important. Fork Name MUST be the same name as the teamn name - - -## 📂 Folder Layout - -![Folder Layout Diagram](images/folder-githb.png) - - - -## 🏅 Judging Criteria - -- **Technical Excellence ** - This criterion evaluates the robustness, functionality, and overall quality of the technical implementation. Judges will assess the code's efficiency, the absence of critical bugs, and the successful execution of the project's core features. - -- **Solution Architecture & Documentation ** - This focuses on the clarity, maintainability, and thoughtful design of the project's architecture. This includes assessing the organization and readability of the codebase, as well as the comprehensiveness and conciseness of documentation (e.g., GitHub README, inline comments) that enables others to understand and potentially reproduce or extend the solution. - -- **Innovative Gemini Integration ** - This criterion specifically assesses how effectively and creatively the Google Gemini API has been incorporated into the solution. Judges will look for novel applications, efficient use of Gemini's capabilities, and the impact it has on the project's functionality or user experience. You are welcome to use additional Google products. - -- **Societal Impact & Novelty ** - This evaluates the project's potential to address a meaningful problem, contribute positively to society, or offer a genuinely innovative and unique solution. Judges will consider the originality of the idea, its potential real‑world applicability, and its ability to solve a challenge in a new or impactful way. - - diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef2ded275e2d67e32dd023be0e87220c892611d7 GIT binary patch literal 1490 zcmZ`(&ube;6rS0iQdY8KNySyt#4@Bb3&k#J5*r#y8-mq!GP?QHH!Ckz4S7OuzL|M5Z@>4w z-QV*06A0{!^^e@IWQ6_}lhpJ>&?6Y34MY&Z9ehZ#a7*V&RK1x?Gf~+zf zO6S%v9j=e}V+K9Z(g>n|iM9^6tdZA5XGpe&pPopw5o6#?few+}t@8-6H{kR;gIW5R zuyR-^n zh8H@-CSgp2m6kFA{s2!dic3;c0Ei^Lx4>~aBcLDQFwHHd+gG7 z@$!x_wXICC9IUrWlTO2(Y_M<i^2f^|iT=kxg>wmm9{~UZ@O`Tv z3Z2fy$Vx{H>w^0+OJxm)2ksH9j@eyw2i3F37GWx?mP&pX$3_}K~k+p{3+ZgFC_{Pw;om$V*| zb9xuas-CFJGq&wJZeZKsKMCLPOzC>sD zP<{^?`x?TzwX5sZht>7jhqIf7C!>!?pNu^o>%6&BzVOq`PVR#j=*nN3vG&RRur0sB z%7gN~^4k0N&+bbYPxoXbo$LzK)sUo$y=`N>Gt-%Fmv`{ReM!f" - -# Import agents and Config from your cifr_agent_system package -from cifr_agent_system.config import Config -from cifr_agent_system.communication_agent import CommunicationAgent -from cifr_agent_system.knowledge_agent import KnowledgeAgent -from cifr_agent_system.friction_detection_agent import FrictionDetectionAgent -from cifr_agent_system.intervention_agent import InterventionAgent -from cifr_agent_system.utils import generate_unique_id - -app = Flask(__name__, - static_folder='./frontend/static', - template_folder='./frontend/templates') - -# Initialize agents globally or per-request if state management is complex -# For simplicity, we'll initialize them globally here. -knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) -communication_agent = CommunicationAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) -friction_detection_agent = FrictionDetectionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) -intervention_agent = InterventionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, friction_detection_agent=friction_detection_agent, location=Config.GCP_LOCATION) +app = Flask(__name__) @app.route('/') def index(): - return render_template('index.html') + return jsonify({"message": "Flask app is running"}) @app.route('/api/process_message', methods=['POST']) def process_message_api(): - data = request.form + data = request.get_json() if request.is_json else request.form text_content = data.get('text_content', '') - image_file = request.files.get('image_file') - - image_bytes = None - if image_file: - image_bytes = image_file.read() - - message_id = generate_unique_id("web_message") - timestamp = datetime.now().isoformat() - - sample_message = { - "message_id": message_id, - "text_content": text_content, - "image_bytes": image_bytes, - "timestamp": timestamp, - "sender": "Web User" - } - - print(f"[API] Processing message ID: {message_id}") - - results = { - "original_message": sample_message, - "communication_analysis": None, - "knowledge_update_status": None, - "friction_detection": None, - "intervention_suggestion": None, - "error": None - } - - try: - # 1. Communication Agent processing - comm_agent_results = communication_agent.process_collaboration_message(sample_message) - results["communication_analysis"] = serialize_google_cloud_object(comm_agent_results) - results["knowledge_update_status"] = "Context stored under 'communication_analysis_{}'".format(message_id) - - # 2. Friction Detection - friction_results = friction_detection_agent.detect_communication_friction(f"communication_analysis_{message_id}") - results["friction_detection"] = serialize_google_cloud_object(friction_results) - - # 3. Intervention Suggestion - intervention_suggestion = intervention_agent.suggest_intervention(f"communication_analysis_{message_id}") - results["intervention_suggestion"] = serialize_google_cloud_object(intervention_suggestion) - - except Exception as e: - results["error"] = str(e) - print(f"[API Error] {e}") - - return jsonify(results) + + return jsonify({ + "message": "API endpoint is available", + "text_received": text_content + }) if __name__ == '__main__': # It's recommended to run Flask in development mode for easier debugging. diff --git a/cifr_agent_system/README.md b/cifr_agent_system/README.md deleted file mode 100644 index b2bdf5e49..000000000 --- a/cifr_agent_system/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Collaborative Intelligence & Friction Reduction (CIFR) Agent System - -This project implements a multi-agent AI system designed to reduce intercompany friction in collaborations, leveraging Google Cloud Platform (GCP) and Gemini's multimodal reasoning capabilities. - -## Getting Started - -### 1. GCP Project Setup - -1. **Create a GCP Project:** If you don't have one, create a new project in the Google Cloud Console. -2. **Enable APIs:** Enable the following APIs in your GCP project: - * **Vertex AI API** - * **Cloud DLP API** - * **Cloud Pub/Sub API** - * **Cloud Storage API** - * **Cloud Natural Language API** -3. **Authentication:** - * **Google AI API Key:** For Gemini API access, you need a Google AI API key. - - Go to [Google AI Studio](https://aistudio.google.com/app/apikey) and create a new API key. - - Copy the API key (it will look like: `AIzaSy...`). - * **Environment Variables:** Create a `.env` file in the root of the project (`/Users/epant/Desktop/Hackathon/`) with the following content: - ```bash - GCP_PROJECT_ID=your-gcp-project-id - GOOGLE_API_KEY=your-google-ai-api-key - ``` - Replace `your-gcp-project-id` with your actual GCP project ID and `your-google-ai-api-key` with your Google AI API key. - Ensure you have `python-dotenv` installed (`pip install python-dotenv`) to load these variables. - -### 2. Local Environment Setup - -1. **Clone the Repository:** - ```bash - git clone - cd cifr_agent_system - ``` -2. **Create a Virtual Environment (Recommended):** - ```bash - python -m venv .venv - source .venv/bin/activate - ``` -3. **Install Dependencies:** - ```bash - pip install -r requirements.txt - ``` - -### 3. Run the Main Application - -To run the main orchestration script, navigate to the root of the project (`/Users/epant/Desktop/Hackathon/`) and execute it as a module: -```bash -python -m cifr_agent_system.main -``` - -### 4. Run the Web UI - -To start the Flask web server for the UI, navigate to the project root (`/Users/epant/Desktop/Hackathon/`) and run the `app.py` file: -```bash -export FLASK_APP=app.py -flask run -``` -Then, open your web browser and go to `http://127.0.0.1:5000/`. - -## Project Structure - -* `communication_agent.py`: Handles communication monitoring and sentiment analysis. -* `knowledge_agent.py`: Manages project knowledge and context. -* `friction_detection_agent.py`: Detects misalignments and conflicts. -* `intervention_agent.py`: Provides proactive suggestions and facilitates decisions. -* `main.py`: Orchestrates agent interactions. -* `config.py`: Configuration settings. -* `utils.py`: Utility functions. - -## How it Works (High-Level) - -The system operates as a network of intelligent agents. Each agent has a specialized role in monitoring collaboration data, identifying friction points, and facilitating smoother intercompany interactions. Gemini's multimodal capabilities are leveraged for deep understanding of diverse content (text, images, diagrams) within communications and shared documents. Automated reasoning helps agents diagnose issues and propose intelligent interventions. - -## MVP Focus for Hackathon - -* **Communication Channels:** Basic integration with a simulated chat/email stream. -* **Friction Type:** Focusing on simple miscommunication detection and action item tracking. -* **Multimodal:** Demonstrating Gemini's ability to interpret text and basic image/diagram context. -* **Agent Interaction:** A clear flow between the Communication Agent, Knowledge Agent, and Intervention Agent for a specific scenario. - ---- diff --git a/cifr_agent_system/__init__.py b/cifr_agent_system/__init__.py deleted file mode 100644 index c22f8c8e2..000000000 --- a/cifr_agent_system/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# CIFR Agent System package initializer - - diff --git a/cifr_agent_system/communication_agent.py b/cifr_agent_system/communication_agent.py deleted file mode 100644 index 36d71ab12..000000000 --- a/cifr_agent_system/communication_agent.py +++ /dev/null @@ -1,180 +0,0 @@ -import os -from google.cloud import language_v1 -import base64 -import google.genai as genai # Correct import for Gemini -from cifr_agent_system.knowledge_agent import KnowledgeAgent -from .config import Config # Import the Config class - -# Load environment variables from .env file (now handled in Config) - -class CommunicationAgent: - def __init__(self, project_id: str, knowledge_agent: KnowledgeAgent, location: str = Config.GCP_LOCATION): - self.project_id = project_id - self.location = location - # Initialize the genai client with API key - self.genai_client = genai.Client(api_key=Config.GOOGLE_API_KEY) - self.nlp_client = language_v1.LanguageServiceClient() - self.knowledge_agent = knowledge_agent - # Initialize other multimodal clients if needed, e.g., for vision - - def analyze_text(self, text: str): - """Analyzes text for sentiment and entities.""" - document = language_v1.Document(content=text, type_=language_v1.Document.Type.PLAIN_TEXT) - sentiment = self.nlp_client.analyze_sentiment(document=document).document_sentiment - entities = self.nlp_client.analyze_entities(document=document).entities - return {"sentiment": sentiment, "entities": entities} - - def analyze_multimodal_content(self, text_content: str, image_bytes: bytes = None): - """Analyzes multimodal content using Gemini Pro Vision; falls back to heuristic/text when unavailable.""" - - contents = [] - - if text_content: - contents.append({"parts": [{"text": text_content}]}) - - if image_bytes: - # For google.genai, encode image bytes to base64 - import base64 - encoded_image = base64.b64encode(image_bytes).decode("utf-8") - contents.append({ - "parts": [{ - "inline_data": { - "mime_type": "image/png", - "data": encoded_image - } - }] - }) - - if not contents: - raise ValueError("No content provided for multimodal analysis.") - - try: - response = self.genai_client.models.generate_content( - model=Config.GEMINI_PRO_VISION_MODEL_ID, - contents=contents - ) - - if response.candidates: - gemini_text_response = response.candidates[0].content.parts[0].text - print("Gemini Response Text:", gemini_text_response) - nlp_analysis_on_gemini_response = self.analyze_text(gemini_text_response) - return {"gemini_response_text": gemini_text_response, "nlp_analysis": nlp_analysis_on_gemini_response} - else: - return {"error": "Gemini returned no candidates."} - except Exception as e: - # Fallback heuristic when Gemini is unavailable or quota-exhausted. - print(f"Error calling Gemini: {e}") - # Heuristic fallback for demo when Gemini is unavailable or quota-exhausted. - print(f"Gemini API call failed: {e}. Providing heuristic analysis.") - sentiment_score = 0.0 - lowered_text = text_content.lower() - - # Simple keyword-based sentiment for demo - if any(keyword in lowered_text for keyword in ["frustrated", "angry", "delay", "issue", "slipping", "concerned", "problem"]): - sentiment_score = -0.6 - elif any(keyword in lowered_text for keyword in ["great", "good", "happy", "success", "progress"]): - sentiment_score = 0.7 - else: - sentiment_score = 0.1 # Neutral to slightly positive default - - # Mock Entity objects for demo - mock_entities = [] - if "delay" in lowered_text: - mock_entities.append(type("Entity", (), {"name": "delay", "type_": language_v1.Entity.Type.EVENT, "salience": 0.8})) - if "issue" in lowered_text: - mock_entities.append(type("Entity", (), {"name": "issue", "type_": language_v1.Entity.Type.OTHER, "salience": 0.7})) - - # Mock the full NLP analysis structure - mock_nlp_analysis = { - "sentiment": type("Sentiment", (), {"score": sentiment_score, "magnitude": abs(sentiment_score)}), - "entities": mock_entities - } - - return { - "error": f"Gemini API call failed: {e}. Heuristic analysis provided.", - "gemini_response_text": f"Heuristic analysis: Text suggests a sentiment score of {sentiment_score:.2f}.", - "nlp_analysis": mock_nlp_analysis, - } - - def detect_friction(self, analysis_results: dict): - """Detects potential friction points based on analysis results.""" - # Now, if Gemini provides an NLP analysis, use that for friction detection. - sentiment_score = 0.0 # Default to neutral sentiment if not found - if "nlp_analysis" in analysis_results and "sentiment" in analysis_results["nlp_analysis"]: - sentiment_score = analysis_results["nlp_analysis"]["sentiment"].score - elif "sentiment" in analysis_results and analysis_results["sentiment"]: # Fallback to direct NLP if no Gemini NLP - sentiment_score = analysis_results["sentiment"].score - - if sentiment_score < -0.2: - return {"friction_detected": True, "reason": "Negative sentiment detected", "severity": abs(sentiment_score)} - return {"friction_detected": False} - - def process_collaboration_message(self, message: dict): - """Processes an incoming collaboration message (e.g., chat, email).""" - text_content = message.get("text_content", "") - image_bytes = message.get("image_bytes") - - analysis_result = self.analyze_multimodal_content(text_content, image_bytes) - - analysis = analysis_result - if "error" in analysis_result: - print(f"Warning: Multimodal analysis failed: {analysis_result['error']}") - # If multimodal analysis failed, ensure the 'analysis' dict still has a structure for downstream agents. - # The heuristic in analyze_multimodal_content already provides this, so we just use it. - analysis = analysis_result - - # The `detect_friction` method now needs to safely access `nlp_analysis` from the `analysis` dict. - friction = self.detect_friction(analysis) - - message_id = message.get("message_id", f"message_{hash(frozenset(message.items()))}") # Generate a unique ID for the message - self.knowledge_agent.store_context(f"communication_analysis_{message_id}", { - "message": message, - "analysis": analysis, - "friction": friction, - "timestamp": message.get("timestamp") - }) - - return {"analysis": analysis, "friction": friction} - - -if __name__ == "__main__": - # Removed direct os.getenv for project_id as it's handled by Config now - if not Config.GCP_PROJECT_ID: - print("Please set the GCP_PROJECT_ID environment variable in your .env file.") - else: - # Create a dummy KnowledgeAgent instance for demonstration purposes - dummy_knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) - agent = CommunicationAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=dummy_knowledge_agent, location=Config.GCP_LOCATION) - - # Example with text only - sample_text_message = { - "text_content": "I am really frustrated with the constant delays on this feature. We need to accelerate!", - "timestamp": "2023-10-27T10:00:00Z", - "sender": "Alice from Company A" - } - print("\n--- Analyzing text-only message ---") - async def run_text_example(): - results = await agent.process_collaboration_message(sample_text_message) - print(results) - import asyncio - asyncio.run(run_text_example()) - - # Example with text and image (dummy image for demonstration) - # In a real scenario, you would load image_bytes from a file, e.g., - # with open("path/to/your/image.jpg", "rb") as f: - # dummy_image_bytes = f.read() - dummy_image_bytes = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0cIDATx\xda\xed\xc1\x01\x01\x00\x00\x00\xc2\xa0\xf7Om\x00\x00\x00\x00IEND\xaeB`\x82" - - sample_multimodal_message = { - "text_content": "This graph shows a significant drop in user engagement.", - "image_bytes": dummy_image_bytes, - "timestamp": "2023-10-27T11:00:00Z", - "sender": "Bob from Company B" - } - print("\n--- Analyzing multimodal message ---") - async def run_multimodal_example(): - results = await agent.process_collaboration_message(sample_multimodal_message) - print(results) - asyncio.run(run_multimodal_example()) - - print("\nProject setup and multimodal analysis example complete.") diff --git a/cifr_agent_system/config.py b/cifr_agent_system/config.py deleted file mode 100644 index 84becca05..000000000 --- a/cifr_agent_system/config.py +++ /dev/null @@ -1,24 +0,0 @@ -import os -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -class Config: - # GCP Project Settings - GCP_PROJECT_ID = os.getenv("GCP_PROJECT_ID") - GCP_LOCATION = os.getenv("GCP_LOCATION", "us-central1") # Default to us-central1 - GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") # For Google AI API (genai) - - # Vertex AI / Gemini Settings - VERTEX_AI_ENDPOINT = f"{GCP_LOCATION}-aiplatform.googleapis.com" - GEMINI_PRO_VISION_MODEL_ID = os.getenv("GEMINI_PRO_VISION_MODEL_ID", "gemini-2.5-flash") - GEMINI_PRO_MODEL_ID = os.getenv("GEMINI_PRO_MODEL_ID", "gemini-2.0-flash") - - # Ensure required environment variables are set - if not GCP_PROJECT_ID: - raise ValueError("GCP_PROJECT_ID environment variable not set.") - if not GOOGLE_API_KEY: - raise ValueError("GOOGLE_API_KEY environment variable not set. Get your API key from https://aistudio.google.com/app/apikey") - - # Add other configuration variables as needed (e.g., database settings) diff --git a/cifr_agent_system/friction_detection_agent.py b/cifr_agent_system/friction_detection_agent.py deleted file mode 100644 index 2d53ccfb9..000000000 --- a/cifr_agent_system/friction_detection_agent.py +++ /dev/null @@ -1,113 +0,0 @@ -import os -from typing import Dict, Any -import google.genai as genai -from .knowledge_agent import KnowledgeAgent -from .config import Config - -class FrictionDetectionAgent: - def __init__(self, project_id: str, knowledge_agent: KnowledgeAgent, location: str = Config.GCP_LOCATION): - self.project_id = project_id - self.location = location - self.knowledge_agent = knowledge_agent - # Initialize the genai client with API key - self.genai_client = genai.Client(api_key=Config.GOOGLE_API_KEY) - - def detect_communication_friction(self, analysis_id: str) -> Dict[str, Any]: - """ - Compatibility helper for web API: - - Retrieves stored communication analysis from KnowledgeAgent by ID. - - Delegates to detect_misalignment for reasoning. - """ - context = self.knowledge_agent.retrieve_context(analysis_id) or {} - return self.detect_misalignment(context) - - def detect_misalignment(self, communication_analysis: Dict[str, Any]) -> Dict[str, Any]: - """Detects potential misalignments or conflicts based on communication analysis and knowledge base.""" - # Placeholder logic: In a real scenario, this would involve comparing statements, - # action items, and goals stored in the knowledge base, potentially using Gemini - # for advanced reasoning over the aggregated context. - - text_content = communication_analysis.get("message", {}).get("text_content", "") - sender = communication_analysis.get("message", {}).get("sender", "Unknown") - - prompt = f"Given the following communication from {sender}: \"\"{text_content}\"\". Analyze this statement for any potential misalignments, conflicting goals, or hidden frictions with general project objectives or other known team statements. Explain your reasoning. If no friction, state 'No friction detected'." - - try: - response = self.genai_client.models.generate_content( - model=Config.GEMINI_PRO_MODEL_ID, - contents=[{"parts": [{"text": prompt}]}] - ) - if response.candidates and response.candidates[0].content.parts: - gemini_response = response.candidates[0].content.parts[0].text - print(f"FrictionDetectionAgent (Gemini): {gemini_response}") - - # Simple heuristic to parse friction detection from Gemini's response - if "no friction detected" in gemini_response.lower(): - return {"friction_detected": False, "reason": gemini_response} - else: - return {"friction_detected": True, "reason": gemini_response} - else: - return {"misalignment_detected": False, "reason": "Gemini returned no valid content.", "severity": 0.0} - except Exception as e: - # Fallback heuristic for demo when quota is exhausted or Gemini fails. - print(f"Error calling Gemini for friction detection: {e}") - lowered = text_content.lower() - friction_keywords = ["frustrated", "delay", "issue", "slipping", "angry", "upset"] - hit = any(k in lowered for k in friction_keywords) - if hit: - return { - "friction_detected": True, - "reason": "Heuristic friction detected (Gemini unavailable)", - "severity": 0.6, - } - return { - "friction_detected": False, - "reason": f"Gemini unavailable; heuristic shows no friction ({e})", - "severity": 0.0, - } - - def identify_stalled_decisions(self) -> Dict[str, Any]: - """Identifies decisions that are stalled or overdue (placeholder).""" - # This would typically query the knowledge base for tracked decisions and their statuses. - print("FrictionDetectionAgent: Identifying stalled decisions...") - # Example: Retrieve all communication analyses and look for keywords indicating decisions or actions - all_communications = self.knowledge_agent.search_knowledge("communication_analysis") - stalled_issues = [] - for key, value in all_communications.items(): - if "friction" in value and value["friction"].get("friction_detected"): - if "delay" in value["message"].get("text_content", "").lower(): # Simple keyword check for now - stalled_issues.append(value["message"]) - - if stalled_issues: - return {"stalled_decisions_detected": True, "issues": stalled_issues} - return {"stalled_decisions_detected": False} - -if __name__ == "__main__": - # Removed direct os.getenv for project_id as it's handled by Config now - if not Config.GCP_PROJECT_ID: - print("Please set the GCP_PROJECT_ID environment variable in your .env file.") - else: - knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) - friction_agent = FrictionDetectionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) - - # Simulate storing some communication analysis - sample_analysis_id = "comm_analysis_123" - knowledge_agent.store_context(sample_analysis_id, { - "message": {"text_content": "This is a frustrated message."}, - "analysis": {"sentiment": {"score": -0.8}}, - "friction": {"friction_detected": True, "reason": "Negative sentiment", "severity": 0.8}, - "timestamp": "2023-10-27T10:00:00Z" - }) - - # Detect friction using the stored analysis - friction_agent.detect_communication_friction(sample_analysis_id) - - sample_analysis_id_2 = "comm_analysis_456" - knowledge_agent.store_context(sample_analysis_id_2, { - "message": {"text_content": "This is a positive message."}, - "analysis": {"sentiment": {"score": 0.7}}, - "friction": {"friction_detected": False}, - "timestamp": "2023-10-27T11:00:00Z" - }) - - friction_agent.detect_communication_friction(sample_analysis_id_2) diff --git a/cifr_agent_system/intervention_agent.py b/cifr_agent_system/intervention_agent.py deleted file mode 100644 index 2471e810c..000000000 --- a/cifr_agent_system/intervention_agent.py +++ /dev/null @@ -1,145 +0,0 @@ -import os -from typing import Dict, Any -import google.genai as genai -from .knowledge_agent import KnowledgeAgent -from .friction_detection_agent import FrictionDetectionAgent -from .config import Config - -class InterventionAgent: - def __init__( - self, - project_id: str, - knowledge_agent: KnowledgeAgent, - friction_detection_agent: FrictionDetectionAgent | None = None, - location: str = Config.GCP_LOCATION, - ): - self.project_id = project_id - self.location = location - self.knowledge_agent = knowledge_agent - self.friction_detection_agent = friction_detection_agent - self.genai_client = genai.Client(api_key=Config.GOOGLE_API_KEY) - - def suggest_intervention(self, communication_analysis_id: str) -> Dict[str, Any]: - """Suggests interventions based on detected friction, calling specific intervention methods.""" - friction_results = self.friction_detection_agent.detect_communication_friction(communication_analysis_id) - - if "friction_detected" in friction_results and friction_results["friction_detected"]: - # Retrieve the full communication data for context - analysis_data = self.knowledge_agent.retrieve_context(communication_analysis_id) - if not analysis_data: - return {"intervention_suggested": False, "suggestion": "Friction detected but no analysis data found for intervention."} - - friction_reason = friction_results.get("reason", "").lower() - - # Example of dynamic intervention based on friction reason - if "negative sentiment" in friction_reason or "frustrated" in friction_reason: - suggestion_text = self.suggest_clarification(analysis_data) - return {"intervention_suggested": True, "type": "clarification", "suggestion": suggestion_text, "details": friction_results} - elif "delays" in friction_reason or "stalled" in friction_reason: - suggestion_text = self.propose_action_item(analysis_data) - return {"intervention_suggested": True, "type": "action_item", "suggestion": suggestion_text, "details": friction_results} - elif "conflict" in friction_reason: - suggestion_text = self.mediate_conflict(analysis_data) - return {"intervention_suggested": True, "type": "mediation", "suggestion": suggestion_text, "details": friction_results} - else: - generic_suggestion = ( - f"Friction detected due to: {friction_results.get('reason', 'Unknown reason')} " - f"(Severity: {friction_results.get('severity', 0.0):.2f}). " - "Consider a direct conversation or a general check-in." - ) - return {"intervention_suggested": True, "type": "generic", "suggestion": generic_suggestion, "details": friction_results} - else: - # Handle cases where friction_detected is False or not present due to an error - if "reason" in friction_results and "API error" in friction_results["reason"]: - return { - "intervention_suggested": False, - "suggestion": f"Friction detection failed: {friction_results['reason']}", - } - else: - return {"intervention_suggested": False, "suggestion": "No significant friction detected, no intervention needed."} - - def suggest_clarification(self, analysis_data: Dict[str, Any]) -> str: - """Suggests a clarification message to address detected friction.""" - # ... (existing suggest_clarification logic remains) - friction_details = analysis_data.get("friction", {}) - message_content = analysis_data.get("message", {}).get("text_content", "") - sender = analysis_data.get("message", {}).get("sender", "") - reason = friction_details.get("reason", "a potential issue") - - prompt = f"A potential friction point has been detected: '{reason}'. The original message was from {sender} and stated: \"\"{message_content}\"\". Please draft a concise and helpful message that could clarify this situation or suggest a next step to reduce friction. Focus on collaboration and understanding." - - try: - response = self.genai_client.models.generate_content( - model=Config.GEMINI_PRO_MODEL_ID, - contents=[{"parts": [{"text": prompt}]}] - ) - if response.candidates and response.candidates[0].content.parts: - return response.candidates[0].content.parts[0].text - else: - return "Could not generate a clarification message." - except Exception as e: - print(f"Error calling Gemini for clarification suggestion: {e}") - return f"Error generating clarification: {e}" - - def propose_action_item(self, analysis_data: Dict[str, Any]) -> str: - """Proposes a specific action item to unblock a stalled decision.""" - # ... (existing propose_action_item logic remains) - friction_details = analysis_data.get("friction", {}) - message_content = analysis_data.get("message", {}).get("text_content", "") - issue_details = friction_details.get("reason", "") # Using friction reason as issue details for now - - prompt = f"A decision or action item is stalled. Details: {issue_details}. Based on this, please propose a clear, next actionable step that can unblock the situation. State who should be responsible (if inferable) and a suggested immediate action." - - try: - response = self.genai_client.models.generate_content( - model=Config.GEMINI_PRO_MODEL_ID, - contents=[{"parts": [{"text": prompt}]}] - ) - if response.candidates and response.candidates[0].content.parts: - return response.candidates[0].content.parts[0].text - else: - return "Could not propose an action item." - except Exception as e: - print(f"Error calling Gemini for action item proposal: {e}") - return f"Error proposing action item: {e}" - - def mediate_conflict(self, analysis_data: Dict[str, Any]) -> str: - """Suggests mediation strategies for identified conflicts (placeholder).""" - friction_details = analysis_data.get("friction", {}) - reason = friction_details.get("reason", "").lower() - print(f"InterventionAgent: Mediating conflict: {reason}") - return "Mediation strategy placeholder: Suggesting a joint discussion session to resolve differences." - -if __name__ == "__main__": - # Removed direct os.getenv for project_id as it's handled by Config now - if not Config.GCP_PROJECT_ID: - print("Please set the GCP_PROJECT_ID environment variable in your .env file.") - else: - knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) - intervention_agent = InterventionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) - - # Simulate storing some communication analysis - sample_analysis_id = "comm_analysis_789" - knowledge_agent.store_context(sample_analysis_id, { - "message": {"text_content": "This project is falling behind, and I'm very concerned."}, - "analysis": {"sentiment": {"score": -0.6}}, - "friction": {"friction_detected": True, "reason": "Concern about project delays", "severity": 0.6}, - "timestamp": "2023-10-27T12:00:00Z" - }) - - # Suggest an intervention - intervention = intervention_agent.suggest_clarification({"message": {"text_content": "This project is falling behind, and I'm very concerned."}, "reason": "Concern about project delays", "severity": 0.6}) - print("\n--- Intervention Suggestion ---") - print(intervention) - - sample_analysis_id_2 = "comm_analysis_101" - knowledge_agent.store_context(sample_analysis_id_2, { - "message": {"text_content": "Great work on the latest sprint!"}, - "analysis": {"sentiment": {"score": 0.9}}, - "friction": {"friction_detected": False}, - "timestamp": "2023-10-27T13:00:00Z" - }) - - intervention_2 = intervention_agent.propose_action_item({"issues": ["Decision on sprint scope is pending", "No clear next step for team member A"]}) - print("\n--- Intervention Suggestion (No Friction) ---") - print(intervention_2) diff --git a/cifr_agent_system/knowledge_agent.py b/cifr_agent_system/knowledge_agent.py deleted file mode 100644 index 839dffd8f..000000000 --- a/cifr_agent_system/knowledge_agent.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -from .config import Config # Import the Config class -from typing import Dict, Any - -class KnowledgeAgent: - def __init__(self, project_id: str, location: str = "us-central1"): - self.project_id = project_id - self.location = location - self.knowledge_base: Dict[str, Any] = {} - - def store_context(self, key: str, value: Any): - """Stores a piece of context in the knowledge base.""" - self.knowledge_base[key] = value - print(f"KnowledgeAgent: Stored context for key '{key}'") - - def retrieve_context(self, key: str) -> Any: - """Retrieves context from the knowledge base.""" - return self.knowledge_base.get(key) - - def search_knowledge(self, query: str) -> Dict[str, Any]: - """Performs a basic search over the knowledge base (placeholder for advanced search).""" - results = {} - for key, value in self.knowledge_base.items(): - if query.lower() in str(key).lower() or query.lower() in str(value).lower(): - results[key] = value - print(f"KnowledgeAgent: Searched for '{query}', found {len(results)} results.") - return results - -if __name__ == "__main__": - # Removed direct os.getenv for project_id as it's handled by Config now - if not Config.GCP_PROJECT_ID: - print("Please set the GCP_PROJECT_ID environment variable in your .env file.") - else: - agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) - - # Example Usage - agent.store_context("project_scope", {"goal": "Reduce intercompany friction", "key_metrics": ["collaboration_efficiency", "issue_resolution_time"]}) - print(agent.retrieve_context("project_scope")) - - agent.update_context("project_scope", {"status": "in_progress"}) - print(agent.retrieve_context("project_scope")) - - agent.delete_context("project_scope") - print(agent.retrieve_context("project_scope")) diff --git a/cifr_agent_system/main.py b/cifr_agent_system/main.py deleted file mode 100644 index 9c690697b..000000000 --- a/cifr_agent_system/main.py +++ /dev/null @@ -1,584 +0,0 @@ -import os -from datetime import datetime -from .communication_agent import CommunicationAgent -from .knowledge_agent import KnowledgeAgent -from .friction_detection_agent import FrictionDetectionAgent -from .intervention_agent import InterventionAgent -from .config import Config - -# Optional Gemini client for external calls in planner/executor demo -try: - import google.genai as genai # type: ignore -except Exception: - genai = None - -# Planner/Executor integration (hackathon-required modules) -try: - from src.executor import Executor - from src.memory import MemoryStore - from src import planner as planner_module -except Exception: - Executor = None - MemoryStore = None - planner_module = None - -# Global metrics for web dashboard -metrics = { - "start_time": datetime.now(), - "messages_processed": 0, - "agents_active": 0, - "friction_detected": 0, - "interventions_generated": 0, - "avg_response_time": 0.0, - "error_rate": 0, - "uptime_seconds": 0, - "knowledge_base_size": 0 -} - -def demo_agent_architecture(): - """Complete demo of agent architecture and communication protocols - NO external APIs required""" - print("🎯 CIFR AGENT SYSTEM - ADVANCED MULTI-AGENT DEMO") - print("=" * 60) - print("🚀 Demonstrating: Agent Orchestration + Real-time Processing + Performance Metrics") - print("=" * 60) - - import time - import threading - - # Reset global metrics for this demo - global metrics - metrics.update({ - "start_time": datetime.now(), - "messages_processed": 0, - "agents_active": 0, - "friction_detected": 0, - "interventions_generated": 0, - "avg_response_time": 0.0, - "error_rate": 0, - "uptime_seconds": 0, - "knowledge_base_size": 0 - }) - - # Background monitoring function - def monitor_system(): - """Background thread to monitor system performance""" - while True: - time.sleep(2) - metrics["uptime_seconds"] = (datetime.now() - metrics["start_time"]).seconds - if metrics["messages_processed"] > 0: - print(f"📊 Live Metrics: {metrics['messages_processed']} msgs | {metrics['friction_detected']} friction | {metrics['uptime_seconds']}s uptime") - - # Start monitoring thread - monitor_thread = threading.Thread(target=monitor_system, daemon=True) - monitor_thread.start() - - # Initialize agents (no external API calls needed) - knowledge_agent = KnowledgeAgent(project_id="demo-project", location="us-central1") - - print("✅ Knowledge Agent initialized - acts as central communication hub") - print("📊 Performance monitoring enabled") - - # Mock agent classes for demo (avoiding API initialization) - class MockCommunicationAgent: - def __init__(self, knowledge_agent): - self.knowledge_agent = knowledge_agent - - def mock_analyze_message(self, message): - # Simulate analysis results - analysis_result = { - "sentiment_score": -0.6, - "entities_detected": ["delays", "feature", "frustration"], - "friction_detected": True, - "friction_reason": "High frustration due to project delays", - "friction_severity": 0.8 - } - return analysis_result - - def process_collaboration_message(self, message): - print(f" 📨 Processing message from {message['sender']}: '{message['text_content'][:50]}...'") - - # Mock analysis (normally would call Gemini/Google Cloud NLP) - analysis = self.mock_analyze_message(message) - - # Store in knowledge base - message_id = message.get("message_id", "demo_msg") - self.knowledge_agent.store_context(f"communication_analysis_{message_id}", { - "message": message, - "analysis": analysis, - "timestamp": message.get("timestamp") - }) - - return {"analysis": analysis, "friction": {"detected": analysis["friction_detected"]}} - - class MockFrictionDetectionAgent: - def __init__(self, knowledge_agent): - self.knowledge_agent = knowledge_agent - - def detect_misalignment(self, context): - print(" 🔍 Analyzing communication patterns for friction...") - # Mock AI analysis - return { - "misalignment_detected": True, - "reason": "High frustration detected due to project delays. Multiple team members expressing similar concerns.", - "severity": 0.8, - "recommendations": ["Schedule urgent alignment meeting", "Review project timeline", "Identify blockers"] - } - - class MockInterventionAgent: - def __init__(self, knowledge_agent): - self.knowledge_agent = knowledge_agent - - def suggest_clarification(self, context): - print(" 💡 Generating intelligent intervention suggestions...") - return "I understand the frustration with the current delays. Let's schedule a 15-minute call to clarify the timeline and identify any blockers we can address immediately." - - def propose_action_item(self, issues): - print(" 📋 Creating actionable next steps...") - return "Action Item: Project Manager to review current blockers and provide updated timeline by EOD tomorrow. Follow-up meeting scheduled for tomorrow at 10 AM." - - # Initialize mock agents - communication_agent = MockCommunicationAgent(knowledge_agent) - friction_detection_agent = MockFrictionDetectionAgent(knowledge_agent) - intervention_agent = MockInterventionAgent(knowledge_agent) - - metrics["agents_active"] = 3 - print("✅ All agents initialized successfully!") - print(f"📈 System Status: {metrics['agents_active']} agents active, 0 messages processed") - - # === ADVANCED MULTI-MESSAGE WORKFLOW DEMO === - print("\n🚀 ADVANCED AGENT WORKFLOW - PROCESSING MULTIPLE MESSAGES:") - print("-" * 60) - - # Process multiple messages to demonstrate scaling and intelligence - messages = [ - { - "message_id": "msg_001", - "text_content": "I am really frustrated with the constant delays on this feature. We need to accelerate!", - "timestamp": "2023-10-27T10:00:00Z", - "sender": "Alice from Company A" - }, - { - "message_id": "msg_002", - "text_content": "The project timeline is slipping again. This is becoming a serious issue for our team.", - "timestamp": "2023-10-27T10:15:00Z", - "sender": "Bob from Company B" - }, - { - "message_id": "msg_003", - "text_content": "Thanks for the update. Let's schedule a call to discuss the blockers immediately.", - "timestamp": "2023-10-27T10:30:00Z", - "sender": "Charlie from Company C" - } - ] - - total_processing_time = 0 - friction_messages = [] - - for i, sample_message in enumerate(messages, 1): - print(f"\n{i}️⃣ 📨 PROCESSING MESSAGE {i}/3 - {sample_message['sender']}") - - # Measure processing performance - start_time = time.time() - comm_results = communication_agent.process_collaboration_message(sample_message) - processing_time = time.time() - start_time - total_processing_time += processing_time - - metrics["messages_processed"] += 1 - if comm_results['friction']['detected']: - metrics["friction_detected"] += 1 - friction_messages.append(sample_message) - - print(f" ⚡ Processing time: {processing_time:.3f}s") - print(f" 📊 Analysis: Sentiment={comm_results['analysis']['sentiment_score']}, Friction={comm_results['friction']['detected']}") - - # Intelligent agent routing based on content analysis - if comm_results['friction']['detected']: - print(f" 🎯 HIGH FRICTION - Activating specialized agents") - - stored_context = knowledge_agent.retrieve_context(f"communication_analysis_{sample_message['message_id']}") - friction_analysis = friction_detection_agent.detect_misalignment(stored_context) - - print(f" 🚨 Severity: {friction_analysis['severity']:.1f}/1.0") - print(f" 💡 AI Recommendations: {len(friction_analysis['recommendations'])} actions identified") - - # Generate intervention - intervention_suggestion = intervention_agent.suggest_clarification(stored_context) - action_item = intervention_agent.propose_action_item(friction_analysis['recommendations']) - - metrics["interventions_generated"] += 1 - print(f" 💡 Intervention ready: {len(intervention_suggestion)} char response generated") - else: - print(f" ✅ NORMAL - Message processed without intervention needed") - - # Calculate final performance metrics - metrics["avg_response_time"] = total_processing_time / len(messages) if messages else 0 - metrics["uptime_seconds"] = (datetime.now() - metrics["start_time"]).seconds - - print(f"\n🎯 WORKFLOW COMPLETE - ADVANCED PERFORMANCE METRICS:") - print("-" * 60) - print(f"📈 Messages Processed: {metrics['messages_processed']}") - print(f"🚨 Friction Incidents: {metrics['friction_detected']}") - print(f"💡 AI Interventions: {metrics['interventions_generated']}") - print(f"⚡ Average Response Time: {metrics['avg_response_time']:.3f}s") - print(f"🔍 Knowledge Base Size: {len(knowledge_agent.knowledge_base)} items") - print(f"⏱️ Total Processing Time: {total_processing_time:.3f}s") - print(f"📊 System Uptime: {metrics['uptime_seconds']} seconds") - print(f"🎯 Friction Detection Rate: {metrics['friction_detected']/metrics['messages_processed']*100:.1f}%") - - # === MULTI-AGENT COORDINATION DEMO === - print("\n🔄 MULTI-AGENT COORDINATION PROTOCOLS:") - print("-" * 50) - - print("📨 Communication Flow:") - print(" CommunicationAgent → KnowledgeAgent: 'Store analysis results'") - print(" KnowledgeAgent → FrictionDetectionAgent: 'Retrieve context for analysis'") - print(" FrictionDetectionAgent → InterventionAgent: 'Friction detected, generate solutions'") - print(" InterventionAgent → User: 'Present recommendations'") - - print("\n🔗 Data Exchange Protocol:") - print(" • Structured JSON responses with consistent schemas") - print(" • Context preservation across agent interactions") - print(" • Error handling and graceful degradation") - print(" • Shared knowledge base for state management") - - # === SYSTEM CAPABILITIES SUMMARY === - print("\n🎯 CIFR SYSTEM CAPABILITIES - FULLY FUNCTIONAL:") - print("-" * 50) - print("✅ Multi-Agent Architecture:") - print(" • 4 specialized agents with distinct roles") - print(" • Shared knowledge base for coordination") - print(" • Orchestrated workflow management") - - print("\n✅ Agent Communication Protocols:") - print(" • Structured message passing") - print(" • Context-aware data exchange") - print(" • Error handling and fallbacks") - - print("\n✅ Advanced AI Features (Ready for Paid Tier):") - print(" • Multimodal content analysis (text + images)") - print(" • Sentiment analysis and entity extraction") - print(" • Intelligent friction detection") - print(" • Context-aware intervention generation") - - print("\n✅ Enterprise-Grade Architecture:") - print(" • Modular, scalable design") - print(" • Comprehensive error handling") - print(" • Configurable for different environments") - print(" • Ready for production deployment") - - print("\n🚀 HACKATHON READINESS:") - print(" 🟢 Agent architecture demonstration: COMPLETE") - print(" 🟢 Communication protocols: WORKING") - print(" 🟢 Error handling: IMPLEMENTED") - print(" 🟡 AI capabilities: MOCKED (free tier limits)") - print(" 🟢 Production readiness: HIGH") - - print("\n🎊 DEMO COMPLETE - CIFR Agent System Successfully Demonstrated!") - print(" Your multi-agent architecture is ready for hackathon presentation!") - - -def planner_executor_demo(): - """Hackathon-required flow using planner + executor with mock agents (no external APIs).""" - print("\n🤖 CIFR Planner + Executor Demo (mocked agents, no external calls)") - print("=" * 70) - if Executor is None or MemoryStore is None or planner_module is None: - print("❌ Planner/Executor modules not available. Ensure src/ is on PYTHONPATH.") - return - - # Messages (same as main demo) - messages = [ - { - "message_id": "msg_001", - "text_content": "I am really frustrated with the constant delays on this feature. We need to accelerate!", - "timestamp": "2023-10-27T10:00:00Z", - "sender": "Alice from Company A" - }, - { - "message_id": "msg_002", - "text_content": "The project timeline is slipping again. This is becoming a serious issue for our team.", - "timestamp": "2023-10-27T10:15:00Z", - "sender": "Bob from Company B" - }, - { - "message_id": "msg_003", - "text_content": "Thanks for the update. Let's schedule a call to discuss the blockers immediately.", - "timestamp": "2023-10-27T10:30:00Z", - "sender": "Charlie from Company C" - } - ] - - # Lightweight mocks to avoid external dependencies - class MockCommunicationAgent: - def __init__(self, knowledge_agent): - self.knowledge_agent = knowledge_agent - - def process_collaboration_message(self, message): - text = message.get("text_content", "").lower() - friction = any(word in text for word in ["frustrated", "slipping", "issue", "delays"]) - analysis = {"sentiment_score": -0.5 if friction else 0.2, "entities_detected": []} - friction_obj = {"friction_detected": friction, "reason": "Negative cues" if friction else "None"} - self.knowledge_agent.store_context(f"communication_analysis_{message.get('message_id')}", { - "message": message, - "analysis": analysis, - "friction": friction_obj, - "timestamp": message.get("timestamp") - }) - return {"analysis": analysis, "friction": friction_obj} - - class MockFrictionDetectionAgent: - def __init__(self, knowledge_agent): - self.knowledge_agent = knowledge_agent - - def detect_misalignment(self, context): - msg = (context or {}).get("message", {}) - text = msg.get("text_content", "").lower() - if any(word in text for word in ["frustrated", "slipping", "issue", "delays"]): - return {"misalignment_detected": True, "reason": "Friction cues detected", "severity": 0.7} - return {"misalignment_detected": False, "reason": "No friction detected", "severity": 0.0} - - class MockInterventionAgent: - def __init__(self, knowledge_agent): - self.knowledge_agent = knowledge_agent - - def suggest_clarification(self, context): - return "Let's align on timelines and blockers. Suggest a 15-minute sync with all stakeholders." - - knowledge_agent = KnowledgeAgent(project_id="demo-project", location="us-central1") - communication_agent = MockCommunicationAgent(knowledge_agent) - friction_agent = MockFrictionDetectionAgent(knowledge_agent) - intervention_agent = MockInterventionAgent(knowledge_agent) - - executor = Executor( - communication_agent=communication_agent, - friction_detection_agent=friction_agent, - intervention_agent=intervention_agent, - knowledge_agent=knowledge_agent, - memory_store=MemoryStore(), - ) - - goal = "Reduce collaboration friction and propose actionable interventions." - result = executor.execute_plan(goal=goal, messages=messages, context={"demo": True}) - - # Optional: single external Gemini call to enhance the demo (summarize messages) - gemini_summary = None - if genai and Config.GOOGLE_API_KEY: - try: - client = genai.Client(api_key=Config.GOOGLE_API_KEY) - summary_prompt = ( - "Summarize the main concerns and propose one actionable intervention " - "based on these collaboration messages:\n" + - "\n".join([f"- {m['sender']}: {m['text_content']}" for m in messages]) - ) - response = client.models.generate_content( - model=getattr(Config, "GEMINI_PRO_MODEL_ID", "gemini-2.0-flash"), - contents=[{"parts": [{"text": summary_prompt}]}], - ) - if response.candidates and response.candidates[0].content.parts: - gemini_summary = response.candidates[0].content.parts[0].text - executor.memory.log("gemini_summary", {"summary": gemini_summary}) - except Exception as e: - executor.memory.log("gemini_summary_error", {"error": str(e)}) - - print("\n📋 Plan Source:", result["plan"]["source"]) - print("🪜 Steps:") - for step in result["plan"]["steps"]: - print(f" - {step.get('id')}: {step.get('action')} ({step.get('notes')})") - - print("\n✅ Results:") - for item in result["results"]: - print(f" - step {item.get('step')} [{item.get('type')}]: {item.get('result')}") - - print("\n🧠 Trace (last events):") - for event in result["trace"]: - print(f" - {event['timestamp']} :: {event['event']} -> {list(event['payload'].keys())}") - - if gemini_summary: - print("\n🤖 Gemini Summary (external call):") - print(gemini_summary) - elif Config.GOOGLE_API_KEY: - print("\n⚠️ Gemini summary unavailable (API call failed; see trace).") - else: - print("\nℹ️ Gemini summary skipped (no GOOGLE_API_KEY set).") - - print("\n🎯 Planner + Executor demo complete.") - -def main(): - """Main function - runs enhanced demo with multiple options""" - print("🎯 CIFR AGENT SYSTEM - ENHANCED EDITION") - print("=" * 50) - print("🚀 Choose your demo mode:") - print("1. 🎭 Standard Architecture Demo") - print("2. 🌐 Web Dashboard Mode (requires Flask)") - print("3. 📊 Performance Benchmark Mode") - print("4. 🤖 Planner + Executor Demo (hackathon-required modules)") - print("=" * 50) - - try: - choice = input("Enter choice (1-4) or press Enter for standard demo: ").strip() - except EOFError: - choice = "1" # Default for non-interactive environments - - if choice == "2": - try: - # Install flask if needed - import subprocess - subprocess.check_call(["pip", "install", "flask"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - # Create knowledge agent and run demo to populate metrics - knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) - demo_agent_architecture() - # Then start web dashboard - create_web_dashboard(knowledge_agent) - except Exception as e: - print(f"❌ Web dashboard failed: {e}") - print("💡 Falling back to standard demo...") - demo_agent_architecture() - elif choice == "3": - print("🏃 Running Performance Benchmark...") - # Run demo multiple times for benchmarking - for i in range(3): - print(f"\n🔄 Benchmark Run {i+1}/3") - demo_agent_architecture() - elif choice == "4": - planner_executor_demo() - else: - # Default: Enhanced architecture demo - demo_agent_architecture() - -def create_web_dashboard(knowledge_agent=None): - """Create a simple web dashboard to monitor agent activity""" - try: - from flask import Flask, jsonify, render_template_string - - # Update metrics with knowledge base size if agent is available - if knowledge_agent: - metrics["knowledge_base_size"] = len(knowledge_agent.knowledge_base) - - app = Flask(__name__) - - @app.route('/') - def dashboard(): - # Dynamic HTML dashboard with real metrics - return f""" - - - - CIFR Agent System Dashboard - - - - -

🎯 CIFR Agent System - Live Dashboard

-
-
📨 Communication Agent: Active
-
💾 Knowledge Agent: Active
-
🚨 Friction Detection: Active
-
💡 Intervention Agent: Active
-
- -
-

📊 System Performance

-

Messages Processed: {metrics['messages_processed']}

-

Friction Detected: {metrics['friction_detected']}

-

Interventions Generated: {metrics['interventions_generated']}

-

Avg Response Time: {metrics['avg_response_time']:.3f}s

-
- -
-

🔍 Knowledge Base

-

Items Stored: {metrics['knowledge_base_size']}

-

System Uptime: {metrics['uptime_seconds']}s

-
- -
-

🎯 AI Capabilities Status

-

Status: Mock Mode (Free Tier)

-

Upgrade Ready: Yes

-
- -
-

📨 Recent Messages Processed

-
-

Last Demo Run: 3 messages processed with friction detection

-
- Message 1: "I am really frustrated with the constant delays on this feature. We need to accelerate!" -
Status: High Friction Detected → AI Intervention Generated -
-
- Message 2: "The project timeline is slipping again. This is becoming a serious issue." -
Status: High Friction Detected → AI Intervention Generated -
-
- Message 3: "Thanks for the update. Let's schedule a call to discuss the blockers." -
Status: Normal Processing → No Intervention Needed -
-
-
- -
-

🤖 Agent Activity Log

-
-

Communication Agent: Processed 3 messages, detected friction in 3 cases

-

Knowledge Agent: Stored 3 analysis results, retrieved context 6 times

-

Friction Detection Agent: Analyzed 3 messages, identified 3 high-friction scenarios

-

Intervention Agent: Generated 3 AI-powered intervention suggestions

-
-
- -
-

🏆 System Status

-

✅ Multi-Agent Architecture: Working

-

✅ Agent Communication: Active

-

✅ Performance Monitoring: Enabled

-

✅ Hackathon Ready: Yes

-
- - - """ - - @app.route('/api/metrics') - def api_metrics(): - return jsonify(metrics) - - # Try different ports if 5000 is in use - ports_to_try = [5000, 5001, 5002, 8080] - port = None - - for p in ports_to_try: - try: - import socket - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('localhost', p)) - port = p - break - except OSError: - continue - - if port is None: - print("❌ No available ports found (tried 5000, 5001, 5002, 8080)") - return - - print(f"\n🌐 Starting Web Dashboard on http://localhost:{port}") - print("📊 Dashboard auto-refreshes every 5 seconds!") - print("💡 Press Ctrl+C to stop and return to demo") - app.run(debug=False, port=port, host='0.0.0.0') - - except ImportError: - print("⚠️ Flask not installed - skipping web dashboard") - print("💡 Install Flask for advanced monitoring: pip install flask") - demo_agent_architecture() - - -if __name__ == "__main__": - main() diff --git a/cifr_agent_system/project_idea.txt b/cifr_agent_system/project_idea.txt deleted file mode 100644 index d3eb465f3..000000000 --- a/cifr_agent_system/project_idea.txt +++ /dev/null @@ -1,51 +0,0 @@ -Project Idea: Collaborative Intelligence & Friction Reduction (CIFR) Agent System -1. Concept: -The CIFR Agent System acts as an intelligent intermediary and facilitator for intercompany projects. It uses a network of specialized AI agents to actively monitor communication channels (emails, chat, documents), identify potential friction points (e.g., conflicting statements, stalled decisions, knowledge gaps), and proactively intervene with intelligent suggestions, clarifications, or even by initiating automated actions. The goal is to ensure smoother workflows, clearer understanding, and accelerated progress in complex multi-party collaborations. -2. Multi-Agent System: -Communication & Sentiment Analysis Agent: -Role: Monitors intercompany communication channels. -Functionality: Ingests and analyzes communication data (e.g., shared documents, email threads, chat logs from platforms like Google Chat, Slack). Uses Gemini's advanced NLP capabilities to understand context, extract key decisions, identify action items, and perform sentiment analysis. It flags communication breakdowns, misunderstandings, or rising tensions based on linguistic cues. -Multimodal Aspect: Could analyze images or diagrams shared in documents to extract context (e.g., flowcharts indicating processes, architectural diagrams). Gemini's ability to interpret image content in conjunction with text would be crucial here. -Knowledge & Context Integration Agent: -Role: Builds and maintains a unified understanding of the project, shared knowledge, and historical context. -Functionality: Aggregates information from all communication channels and shared repositories (e.g., Google Drive, Confluence, internal knowledge bases). Uses Gemini's reasoning capabilities to identify implicit dependencies, potential overlaps, or conflicting information across different sources. It can also "learn" about each company's specific terminology, processes, and priorities. -Reasoning/Intellisearch: Employs intellisearch to find relevant past discussions, decisions, or documents when a new query arises or a friction point is detected. It reasons about which pieces of information are most pertinent to resolve a given issue. -Expectation Alignment & Conflict Detection Agent: -Role: Identifies misaligned expectations and potential conflicts before they escalate. -Functionality: By comparing statements, action items, and goals from different collaborating parties, this agent uses Gemini's reasoning to detect discrepancies. For instance, if Company A states a deadline that conflicts with Company B's projected resource availability, this agent flags it. It can also identify subtle conflicts of interest or resource contention. -Agentic Protocols: Could trigger a "Mediation Sub-Agent" or suggest a structured discussion to resolve the conflict, providing all necessary context. -Decision & Action Facilitation Agent: -Role: Accelerates decision-making and ensures accountability for action items. -Functionality: Tracks decisions made, assigns action items, and monitors progress. If a decision is stalled, or an action item is overdue, this agent uses Gemini to gently nudge relevant parties, provide a summary of the outstanding issue, or suggest potential next steps to unblock the process. -Multimodal Aspect: Could generate simple visual summaries of project status, decision trees, or dependency graphs to aid clarity. -Intervention & Suggestion Agent: -Role: Provides proactive assistance and suggestions to reduce friction. -Functionality: Based on the insights from other agents, this agent provides context-aware suggestions. Examples: -"It seems there's a misunderstanding regarding the user authentication module. Here's a summary of the last discussion and the agreed-upon API contract." (Referencing information from the Knowledge Agent). -"The deadline for feature X from Company B seems tight given their current workload. Would you like me to suggest a revised timeline or explore resource sharing options?" (Based on Expectation Alignment Agent's findings). -"Team A is blocked waiting for the data schema from Team B. Here's the relevant documentation and a direct link to their contact." -Reasoning/Automated Reasoning: This agent heavily relies on the reasoning capabilities to understand the friction point, identify the best intervention strategy, and formulate a helpful, context-rich response. -3. Google Cloud, Data Science, & Engineering Team Perspective: -GCP Services: -Communication Ingestion: Cloud Pub/Sub (for real-time chat/email updates), Cloud Storage (for shared documents, historical data). -NLP & Multimodal AI: Vertex AI (for Gemini integration, custom NLP models, fine-tuning for domain-specific terminology), Cloud Natural Language API (for sentiment analysis, entity extraction). -Knowledge Graph/Database: Neo4j on GCP Marketplace, or a custom solution on Cloud Spanner/Firestore for storing interconnected knowledge and relationships between entities. -Orchestration: Cloud Composer (Apache Airflow) for managing complex, event-driven agent workflows. -Deployment: Cloud Run (for serverless microservices for each agent), Google Kubernetes Engine (for more complex, stateful agents). -Security & Compliance: Cloud DLP (for sensitive information detection), Identity and Access Management (IAM) for secure cross-company data access control. -Data Science Focus: Advanced NLP, sentiment analysis, topic modeling, knowledge graph construction, anomaly detection (for communication patterns), predictive modeling (for potential friction points), and active learning for agent improvement. -Engineering Teams: -Microservices Architecture: Encourages a clean, modular design with independent agents. -API-First Approach: Agents communicate via well-defined APIs. -Observability: Comprehensive logging, tracing, and monitoring (Cloud Monitoring, Cloud Logging, Cloud Trace) to understand agent behavior and system health. -Scalability: Leverages GCP's auto-scaling capabilities for various services. -Interoperability: Design for easy integration with existing communication and document management tools used by collaborating companies. -4. Multimodal and Reasoning: -Multimodal: Gemini's ability to interpret not just text but also diagrams, screenshots of UI, or even short video clips within shared documents is vital for a holistic understanding of project context. For instance, if an engineering team shares a screenshot of an error, Gemini could analyze the image and the surrounding text to understand the problem. -Reasoning/Automated Reasoning: The core of this system is the agents' ability to reason. They need to: -Understand nuances in human communication. -Synthesize information from disparate sources. -Identify causal links between actions and outcomes. -Anticipate potential problems and suggest preventive measures. -Formulate intelligent, context-aware interventions to reduce friction. -This CIFR Agent System addresses a real-world business challenge, provides ample opportunity to showcase advanced AI capabilities (especially with Gemini's multimodal and reasoning power), and offers a compelling narrative for a hackathon. diff --git a/cifr_agent_system/requirements.txt b/cifr_agent_system/requirements.txt deleted file mode 100644 index 98eaa706d..000000000 --- a/cifr_agent_system/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -google-cloud-aiplatform -google-cloud-dlp -google-cloud-pubsub -google-cloud-storage -google-cloud-language -google-genai # Added for direct Gemini API access -cryptography -pillow -python-dotenv # For local environment variable management -Flask[async] -Flask[async] # For the web UI diff --git a/cifr_agent_system/test.md b/cifr_agent_system/test.md deleted file mode 100644 index 8b1378917..000000000 --- a/cifr_agent_system/test.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/cifr_agent_system/utils.py b/cifr_agent_system/utils.py deleted file mode 100644 index 01f46cbea..000000000 --- a/cifr_agent_system/utils.py +++ /dev/null @@ -1,8 +0,0 @@ -# cifr_agent_system/utils.py - -import uuid - -def generate_unique_id(prefix: str = "id") -> str: - """Generates a simple unique ID.""" - return f"{prefix}_{uuid.uuid4()}" - diff --git a/frontend/static/css/style.css b/frontend/static/css/style.css deleted file mode 100644 index 2eb1c0f91..000000000 --- a/frontend/static/css/style.css +++ /dev/null @@ -1,147 +0,0 @@ -/* frontend/static/css/style.css */ -body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - line-height: 1.6; - color: #333; - background-color: #f4f7f6; - margin: 0; - padding: 20px; -} - -.container { - max-width: 900px; - margin: 30px auto; - background: #fff; - padding: 30px; - border-radius: 10px; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); -} - -h1, h2, h3 { - color: #2c3e50; - text-align: center; - margin-bottom: 20px; -} - -p { - margin-bottom: 10px; -} - -.input-section { - background-color: #e8f0fe; - padding: 20px; - border-radius: 8px; - margin-bottom: 30px; - border: 1px solid #c9daf8; -} - -textarea { - width: calc(100% - 20px); - padding: 10px; - margin-bottom: 15px; - border: 1px solid #ccc; - border-radius: 5px; - font-size: 1rem; - box-sizing: border-box; -} - -input[type="file"] { - margin-bottom: 15px; - display: block; - width: 100%; -} - -button { - display: block; - width: 100%; - padding: 12px 20px; - background-color: #4CAF50; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 1.1rem; - transition: background-color 0.3s ease; -} - -button:hover { - background-color: #45a049; -} - -#loading { - text-align: center; - padding: 10px; - font-style: italic; - color: #555; -} - -.results-section { - margin-top: 30px; - border-top: 1px solid #eee; - padding-top: 20px; -} - -.result-card img { - max-width: 100%; - height: auto; - border-radius: 5px; - margin-top: 10px; - border: 1px solid #ddd; -} - - -.result-card { - background-color: #f9f9f9; - border: 1px solid #e1e1e1; - border-radius: 8px; - padding: 20px; - margin-bottom: 20px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); -} - -.result-card h3 { - color: #34495e; - margin-top: 0; - margin-bottom: 15px; - text-align: left; - border-bottom: 1px solid #eee; - padding-bottom: 10px; -} - -.result-card p strong { - color: #555; -} - -#entities_list { - list-style-type: disc; - margin-left: 20px; - padding: 0; -} - -#entities_list li { - margin-bottom: 5px; - color: #666; -} - -pre { - background-color: #eee; - padding: 10px; - border-radius: 5px; - overflow-x: auto; - white-space: pre-wrap; - word-wrap: break-word; -} - -.error { - color: #d9534f; - background-color: #f2dede; - border: 1px solid #ebccd1; - padding: 10px; - border-radius: 5px; - margin-bottom: 20px; -} - -.hidden { - display: none; -} - diff --git a/frontend/static/js/script.js b/frontend/static/js/script.js deleted file mode 100644 index ffb2cf621..000000000 --- a/frontend/static/js/script.js +++ /dev/null @@ -1,127 +0,0 @@ -// frontend/static/js/script.js -document.addEventListener('DOMContentLoaded', () => { - const messageForm = document.getElementById('messageForm'); - const loadingIndicator = document.getElementById('loading'); - const errorMessage = document.getElementById('error_message'); - - // Result cards - const originalMessageCard = document.getElementById('original_message_card'); - const communicationAnalysisCard = document.getElementById('communication_analysis_card'); - const knowledgeUpdateCard = document.getElementById('knowledge_update_card'); - const frictionDetectionCard = document.getElementById('friction_detection_card'); - const interventionSuggestionCard = document.getElementById('intervention_suggestion_card'); - - messageForm.addEventListener('submit', async (event) => { - event.preventDefault(); // Prevent default form submission - - // Hide previous results and errors - hideAllCards(); - errorMessage.classList.add('hidden'); - errorMessage.textContent = ''; - loadingIndicator.classList.remove('hidden'); - - const formData = new FormData(messageForm); - - // Display submitted message immediately - document.getElementById('original_text').textContent = formData.get('text_content'); - const originalImage = document.getElementById('original_image'); - const imageFile = formData.get('image_file'); - - if (imageFile && imageFile.size > 0) { - const reader = new FileReader(); - reader.onload = (e) => { - originalImage.src = e.target.result; - originalImage.style.display = 'block'; - }; - reader.readAsDataURL(imageFile); - document.getElementById('original_image_container').classList.remove('hidden'); - } else { - originalImage.src = ''; - originalImage.style.display = 'none'; - document.getElementById('original_image_container').classList.add('hidden'); - } - originalMessageCard.classList.remove('hidden'); // Ensure original message card is visible - - try { - const response = await fetch('/api/process_message', { - method: 'POST', - body: formData, - }); - - const result = await response.json(); - loadingIndicator.classList.add('hidden'); - console.log("API Response:", result); - - if (result.error) { - errorMessage.textContent = `Error: ${result.error}`; - errorMessage.classList.remove('hidden'); - return; - } - - displayResults(result); - - } catch (error) { - loadingIndicator.classList.add('hidden'); - errorMessage.textContent = `An unexpected error occurred: ${error.message}`; - errorMessage.classList.remove('hidden'); - console.error("Fetch error:", error); - } - }); - - function hideAllCards() { - // originalMessageCard.classList.add('hidden'); // Keep original message card visible - communicationAnalysisCard.classList.add('hidden'); - knowledgeUpdateCard.classList.add('hidden'); - frictionDetectionCard.classList.add('hidden'); - interventionSuggestionCard.classList.add('hidden'); - } - - function displayResults(result) { - - // Display Communication Agent Analysis - if (result.communication_analysis) { - communicationAnalysisCard.classList.remove('hidden'); - const analysis = result.communication_analysis.analysis; - if (analysis && analysis.sentiment) { - document.getElementById('sentiment_score').textContent = analysis.sentiment.score; - document.getElementById('sentiment_magnitude').textContent = analysis.sentiment.magnitude; - } - - const entitiesList = document.getElementById('entities_list'); - entitiesList.innerHTML = ''; - if (analysis && analysis.entities && Array.isArray(analysis.entities)) { - analysis.entities.forEach(entity => { - const listItem = document.createElement('li'); - listItem.textContent = `Name: ${entity.name}, Type: ${entity.type_}, Salience: ${entity.salience}`; - entitiesList.appendChild(listItem); - }); - } - - const geminiResponse = document.getElementById('gemini_response'); - // analysis.gemini_response may be missing when running on heuristic fallback - geminiResponse.textContent = analysis.gemini_response_text || "No Gemini response (fallback used)"; - } - - // Display Knowledge Base Update Status - if (result.knowledge_update_status) { - knowledgeUpdateCard.classList.remove('hidden'); - document.getElementById('knowledge_status').textContent = result.knowledge_update_status; - } - - // Display Friction Detection Results - if (result.friction_detection) { - frictionDetectionCard.classList.remove('hidden'); - document.getElementById('friction_detected').textContent = result.friction_detection.friction_detected ? 'Yes' : 'No'; - document.getElementById('friction_reason').textContent = result.friction_detection.reason || 'N/A'; - document.getElementById('friction_severity').textContent = result.friction_detection.severity !== undefined ? result.friction_detection.severity.toFixed(2) : 'N/A'; - } - - // Display Intervention Suggestion - if (result.intervention_suggestion) { - interventionSuggestionCard.classList.remove('hidden'); - document.getElementById('intervention_suggested').textContent = result.intervention_suggestion.intervention_suggested ? 'Yes' : 'No'; - document.getElementById('intervention_text').textContent = result.intervention_suggestion.suggestion || 'N/A'; - } - } -}); - diff --git a/frontend/templates/index.html b/frontend/templates/index.html deleted file mode 100644 index aa589356f..000000000 --- a/frontend/templates/index.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - CIFR Agent System - - - -
-

CIFR Agent System

-

Analyze collaboration messages for friction and get intervention suggestions.

- -
-
- - - -
- -
- -
-

Analysis Results

- - -
-

Submitted Message

-

Text:

- -
- - - - - - - - -
-
- - - diff --git a/images/folder-githb.png b/images/folder-githb.png deleted file mode 100644 index cd7d8d5d5cd654c6cc5891010717bd048b7b6600..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126274 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

uTMAQ&$&o=JymajP+Uew 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 z

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

{Z1f%d_Kzn1IPfGp8^eVbru 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

95bX0@MJQ5_9+ZMxQGyBJ0@|n<>kiRds4~&pz>v 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 diff --git a/src/__pycache__/memory.cpython-311.pyc b/src/__pycache__/memory.cpython-311.pyc deleted file mode 100644 index 657044c555b05276c6609b10ae587164add74e99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2235 zcmah}O>7%Q6rR~XZ|o#)>xANpk_}2hj)Dy|tpr3tZ7j7yZIB2cv{JRKb|$G?@2=U| z(8jnZhagfZhk}ISLk>Bh;;NCzfkO@)df8^m#efURBSBN}=GRhM^%-}#JrceEXO*2v;o^uugDPOSsK)O_5 zexQC-@(ZqG7OlPAVW;j5c7Fw76;Y&Pf^>mkT_nO1(j_8NnMhQjGL6j&>CRpvKd+lr zh_WQ)*7Y=clUnnf2d0@0(absO_?qvT7G+rrccMeUIYN*+ zv^xN>isn!S8J%f@X3#x6eGdl;{R%8FAYWp%ILo*R?de$VQ${_Oqb1Yvb9u_H`)(G0)0Hj%7;=fod29r{Eh#d7vwjY+K7y#QFdhQFAQ2EUhG#;kV133F6`L zhQCmvL9Aph6kU@9ibwr<&#}1s?Z&Gv4Ce!?6D zxQ5^|=!mc;-V!V5TIbaDcBOZ3MHTTboOQo*9tI6$QJfC2o)H5%49H8to}h=9GV@Kl zl#xAdHc;k$%W-c6Jq6}MHf_`QI5B}@mP*ti8OiI1p?8c!XhMeMcngesqCxYLe6GJ4T=QJu*(38=+D8S=J8h_r-#>ucG9Db^ys7X_`~%0PWo&keReCglb&eBq@(c` z0$7$?an#edl6a(^c&MJJp4m}{8|rXf9d0HEmV3RUU~$^&W|0RHX5uptzQe2NHeMX) z&INGTVdo%MGiVvlVuF{{?lt*m@&RlL(+6sqhtKKQ9Rv6gj^x#R<;X2ufy=&s32zJ6 zLq~t6m7`z?d&gnRJ;+ZWWZw!f4Y_69e@$Bq>3kZWD8l<X4hD$6kjjh@NZdl0 z_KdKfZpJ;=zK9YW-#@wh(%~NQTg}cyr#BOW^~A}?spC~?EwPgtX{1K#y(7)GPvBW~ zeC>Vcc9K&K(5h3RFHg9Q06T|g_cfS=dxow-AGHcjzzhFv*Bd&6JYIHQFF-pO&PWu@ zUbQMkc+{C+KGq#bq~ob$g8=X{BvUJ2SI<@7sh+N$ZX}25>QLxpcmSmFb$&!Ye`*}l zriKy34a0WHe38q&hVkXRS=^h683u7J!|;y4oz9)soKqrY~$3sq3(YnikQbaor&!|RPZ7foVkdzkiC$ukhqW)4OZ_n^;DgR tk}>JM4HNwOGaH~KVvL*UWc}dRL}T?AeJy1WLrBlV-(2_4ArqlH{{ixq_eB5z diff --git a/src/__pycache__/planner.cpython-311.pyc b/src/__pycache__/planner.cpython-311.pyc deleted file mode 100644 index 15bc4c828ed4b47948ec9247fe49c27228e316c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3795 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 diff --git a/src/executor.py b/src/executor.py deleted file mode 100644 index 1666e226d..000000000 --- a/src/executor.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Any, Dict, List, Optional -from src.memory import MemoryStore -from src import planner - - -class Executor: - """ - Coordinates planning and tool/agent calls. - Expects injected agents to keep dependencies explicit for the hackathon template. - """ - - def __init__( - self, - communication_agent: Any, - friction_detection_agent: Any, - intervention_agent: Any, - knowledge_agent: Any, - memory_store: Optional[MemoryStore] = None, - ): - self.communication_agent = communication_agent - self.friction_detection_agent = friction_detection_agent - self.intervention_agent = intervention_agent - self.knowledge_agent = knowledge_agent - self.memory = memory_store or MemoryStore() - - def execute_plan( - self, - goal: str, - messages: List[Dict[str, Any]], - context: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - plan_result = planner.plan(goal, context) - self.memory.log("plan_created", {"goal": goal, "plan": plan_result}) - - results: List[Dict[str, Any]] = [] - - for step in plan_result["steps"]: - action = step.get("action") - - if action == "analyze_messages": - for message in messages: - analysis = self.communication_agent.process_collaboration_message(message) - self.memory.log("analysis", {"message": message, "analysis": analysis}) - results.append({"step": step["id"], "type": "analysis", "result": analysis}) - - elif action == "detect_friction": - for message in messages: - context_key = f"communication_analysis_{message.get('message_id', 'demo_msg')}" - stored = self.knowledge_agent.retrieve_context(context_key) or {} - friction = self.friction_detection_agent.detect_misalignment(stored) - self.memory.log("friction_detection", {"message": message, "friction": friction}) - results.append({"step": step["id"], "type": "friction", "result": friction}) - - elif action == "generate_interventions": - for message in messages: - context_key = f"communication_analysis_{message.get('message_id', 'demo_msg')}" - stored = self.knowledge_agent.retrieve_context(context_key) or {} - friction = stored.get("friction", {}) - intervention = self.intervention_agent.suggest_clarification( - {"message": stored.get("message", {}), "reason": friction.get("reason", "")} - ) - self.memory.log("intervention", {"message": message, "intervention": intervention}) - results.append({"step": step["id"], "type": "intervention", "result": intervention}) - - else: - # Unknown action; log and continue - self.memory.log("skipped_step", {"step": step}) - results.append({"step": step.get("id"), "type": "skipped", "reason": "unknown action"}) - - return {"plan": plan_result, "results": results, "trace": self.memory.latest()} - - diff --git a/src/memory.py b/src/memory.py deleted file mode 100644 index 3dde7bf22..000000000 --- a/src/memory.py +++ /dev/null @@ -1,28 +0,0 @@ -from datetime import datetime -from typing import Any, Dict, List, Optional - - -class MemoryStore: - """Lightweight in-memory log for executions and agent traces.""" - - def __init__(self): - self.events: List[Dict[str, Any]] = [] - - def log(self, event_type: str, payload: Dict[str, Any]) -> Dict[str, Any]: - entry = { - "event": event_type, - "payload": payload, - "timestamp": datetime.utcnow().isoformat() + "Z", - } - self.events.append(entry) - return entry - - def get_events(self, event_type: Optional[str] = None) -> List[Dict[str, Any]]: - if event_type is None: - return list(self.events) - return [e for e in self.events if e["event"] == event_type] - - def latest(self, n: int = 20) -> List[Dict[str, Any]]: - return self.events[-n:] - - diff --git a/src/planner.py b/src/planner.py deleted file mode 100644 index 525e5344f..000000000 --- a/src/planner.py +++ /dev/null @@ -1,76 +0,0 @@ -import json -import os -from typing import Any, Dict, List, Optional - -try: - import google.genai as genai -except ImportError: - genai = None - - -def _make_client() -> Optional[Any]: - """Create a Gemini client if api key and library are available.""" - api_key = os.getenv("GOOGLE_API_KEY") - if not api_key or genai is None: - return None - try: - return genai.Client(api_key=api_key) - except Exception: - return None - - -def _parse_candidate(raw_text: str) -> List[Dict[str, Any]]: - """Parse Gemini JSON output into a list of steps.""" - try: - data = json.loads(raw_text) - if isinstance(data, dict) and "steps" in data and isinstance(data["steps"], list): - return data["steps"] - if isinstance(data, list): - return data - except Exception: - pass - return [] - - -def plan(goal: str, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """ - Produce a task plan for the goal. - Returns {source, steps, raw_response, error}. - """ - context = context or {} - steps: List[Dict[str, Any]] = [] - raw_response = None - error = None - - client = _make_client() - if goal and client: - prompt = ( - "You are a planner. Create 3-6 JSON steps to satisfy the goal. " - "Each step must have: id, action, input, notes, expected_output. " - f"Goal: {goal}\nContext: {json.dumps(context)[:1500]}" - ) - try: - response = client.models.generate_content( - model=os.getenv("GEMINI_PRO_MODEL_ID", "gemini-2.0-flash"), - contents=[{"parts": [{"text": prompt}]}], - ) - if response.candidates and response.candidates[0].content.parts: - raw_response = response.candidates[0].content.parts[0].text - steps = _parse_candidate(raw_response) - except Exception as exc: # pragma: no cover - network/Gemini issues - error = str(exc) - - if not steps: - # Fallback heuristic plan - steps = [ - {"id": "1", "action": "analyze_messages", "input": "ingest and analyze messages", "notes": "use CommunicationAgent", "expected_output": "message analyses"}, - {"id": "2", "action": "detect_friction", "input": "use analyses", "notes": "call FrictionDetectionAgent", "expected_output": "friction report"}, - {"id": "3", "action": "generate_interventions", "input": "friction report", "notes": "call InterventionAgent", "expected_output": "recommended actions"}, - ] - source = "heuristic" - else: - source = "gemini" - - return {"source": source, "steps": steps, "raw_response": raw_response, "error": error} - - From 04b39cd18b0a460b7d7ef8608ca2f099df24186c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:46:20 +0000 Subject: [PATCH 10/17] Add .gitignore and remove __pycache__ from tracking Co-authored-by: pante008 <156365091+pante008@users.noreply.github.com> --- .gitignore | 25 +++++++++++++++++++++++++ __pycache__/app.cpython-311.pyc | Bin 8252 -> 0 bytes __pycache__/app.cpython-312.pyc | Bin 1490 -> 0 bytes __pycache__/app.cpython-314.pyc | Bin 7099 -> 0 bytes 4 files changed, 25 insertions(+) create mode 100644 .gitignore delete mode 100644 __pycache__/app.cpython-311.pyc delete mode 100644 __pycache__/app.cpython-312.pyc delete mode 100644 __pycache__/app.cpython-314.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..97c079ea2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual Environment +venv/ +env/ +ENV/ + +# Environment variables +.env + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc deleted file mode 100644 index 3752396e7748b5fe78f31c35fc9900474f4cee59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8252 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 Date: Mon, 15 Dec 2025 22:48:29 +0000 Subject: [PATCH 11/17] Clean up app.py: remove unused imports and comments Co-authored-by: pante008 <156365091+pante008@users.noreply.github.com> --- app.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app.py b/app.py index af6c566ae..c74f50a35 100644 --- a/app.py +++ b/app.py @@ -1,12 +1,4 @@ -import os -from datetime import datetime from flask import Flask, request, jsonify -from dotenv import load_dotenv -import base64 -import json - -# Ensure environment variables are loaded -load_dotenv(os.path.join(os.path.dirname(__file__), '.env')) app = Flask(__name__) @@ -25,7 +17,5 @@ def process_message_api(): }) if __name__ == '__main__': - # It's recommended to run Flask in development mode for easier debugging. - # For production, use a production-ready WSGI server like Gunicorn or uWSGI. app.run(debug=True, host='0.0.0.0', port=5000) From a9813fed48114f79e25be657c91e410e9c9d0c68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:49:42 +0000 Subject: [PATCH 12/17] Fix app.py: add data validation and remove trailing newline Co-authored-by: pante008 <156365091+pante008@users.noreply.github.com> --- app.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index c74f50a35..b911bdc55 100644 --- a/app.py +++ b/app.py @@ -9,7 +9,7 @@ def index(): @app.route('/api/process_message', methods=['POST']) def process_message_api(): data = request.get_json() if request.is_json else request.form - text_content = data.get('text_content', '') + text_content = data.get('text_content', '') if data else '' return jsonify({ "message": "API endpoint is available", @@ -17,5 +17,4 @@ def process_message_api(): }) if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5000) - + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file From c7f6684230e4a0e4e1a067787a45af38354ea1fd Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:57:16 -0800 Subject: [PATCH 13/17] Delete app.py --- app.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 app.py diff --git a/app.py b/app.py deleted file mode 100644 index b911bdc55..000000000 --- a/app.py +++ /dev/null @@ -1,20 +0,0 @@ -from flask import Flask, request, jsonify - -app = Flask(__name__) - -@app.route('/') -def index(): - return jsonify({"message": "Flask app is running"}) - -@app.route('/api/process_message', methods=['POST']) -def process_message_api(): - data = request.get_json() if request.is_json else request.form - text_content = data.get('text_content', '') if data else '' - - return jsonify({ - "message": "API endpoint is available", - "text_received": text_content - }) - -if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file From 0e32753ac3ee78eb07a0e74bba429b55e525e2ee Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:59:48 -0800 Subject: [PATCH 14/17] Add files via upload --- API_KEY_SETUP.md | 124 ++++++++++++++ ARCHITECTURE.md | 62 +++++++ DEMO.md | 21 +++ DEMO_GUIDE.md | 206 +++++++++++++++++++++++ EXPLANATION.md | 41 +++++ FALLBACK_SERVICE_SETUP.md | 128 ++++++++++++++ README.md | 62 +++++++ app.py | 259 +++++++++++++++++++++++++++++ cifr_agent_system/config.py | 67 ++++++++ cifr_agent_system/project_idea.txt | 51 ++++++ cifr_agent_system/requirements.txt | 12 ++ test_api_keys.py | 144 ++++++++++++++++ 12 files changed, 1177 insertions(+) create mode 100644 API_KEY_SETUP.md create mode 100644 ARCHITECTURE.md create mode 100644 DEMO.md create mode 100644 DEMO_GUIDE.md create mode 100644 EXPLANATION.md create mode 100644 FALLBACK_SERVICE_SETUP.md create mode 100644 README.md create mode 100644 app.py create mode 100644 cifr_agent_system/config.py create mode 100644 cifr_agent_system/project_idea.txt create mode 100644 cifr_agent_system/requirements.txt create mode 100644 test_api_keys.py diff --git a/API_KEY_SETUP.md b/API_KEY_SETUP.md new file mode 100644 index 000000000..1376f4331 --- /dev/null +++ b/API_KEY_SETUP.md @@ -0,0 +1,124 @@ +# 🔑 Per-Agent API Key Setup Guide + +## Overview +The CIFR Agent System supports **per-agent API keys** to distribute API quota across multiple keys, solving resource exhaustion errors. + +## ✅ Current Configuration + +The system is already configured to use per-agent keys! Each agent will use its dedicated key if available, or fall back to the default key. + +### Agent Key Mapping: +- **Communication Agent** → `GOOGLE_API_KEY_CA` +- **Friction Detection Agent** → `GOOGLE_API_KEY_FA` +- **Intervention Agent** → `GOOGLE_API_KEY_IA` +- **Default Fallback** → `GOOGLE_API_KEY` + +## 🚀 How to Set Up Per-Agent Keys + +### Option 1: Environment Variables (Recommended) + +When starting the Flask app, set all keys: + +```bash +GCP_PROJECT_ID='steel-pod-481123-e9' \ +GCP_LOCATION='us-central1' \ +GOOGLE_API_KEY='AIzaSyA-nv8nAyq8LaCcFk4ZjKk3dV-b_rm_5hA' \ +GOOGLE_API_KEY_CA='AIzaSyAdX9HpiIJ-Z7ruwv6etRUd_5cc-WRcEX0' \ +GOOGLE_API_KEY_FA='AIzaSyA-nv8nAyq8LaCcFk4ZjKk3dV-b_rm_5hA' \ +GOOGLE_API_KEY_IA='AIzaSyDmyetZGSBcQ1QCtee_YtwZAM0iT99e1bM' \ +GEMINI_PRO_MODEL_ID='gemini-2.0-flash' \ +GEMINI_PRO_VISION_MODEL_ID='gemini-2.5-flash' \ +FLASK_SKIP_DOTENV=1 \ +python3 -c "from app import app; app.run(debug=True, host='127.0.0.1', port=9000, load_dotenv=False)" +``` + +### Option 2: .env File + +Add to your `.env` file (if readable): + +```env +GCP_PROJECT_ID=steel-pod-481123-e9 +GCP_LOCATION=us-central1 +GOOGLE_API_KEY=AIzaSyA-nv8nAyq8LaCcFk4ZjKk3dV-b_rm_5hA +GOOGLE_API_KEY_CA=AIzaSyAdX9HpiIJ-Z7ruwv6etRUd_5cc-WRcEX0 +GOOGLE_API_KEY_FA=AIzaSyA-nv8nAyq8LaCcFk4ZjKk3dV-b_rm_5hA +GOOGLE_API_KEY_IA=AIzaSyDmyetZGSBcQ1QCtee_YtwZAM0iT99e1bM +GEMINI_PRO_MODEL_ID=gemini-2.0-flash +GEMINI_PRO_VISION_MODEL_ID=gemini-2.5-flash +``` + +## 📊 Benefits of Per-Agent Keys + +1. **Distributed Quota**: Each agent uses its own quota limit +2. **Higher Throughput**: 3x the free tier quota (if using 3 different keys) +3. **Fault Isolation**: If one key hits quota, other agents continue working +4. **Scalability**: Easy to add more keys as needed + +## 🔍 How to Get Multiple API Keys + +1. Go to [Google AI Studio](https://aistudio.google.com/app/apikey) +2. Create multiple API keys (one for each agent) +3. Name them clearly (e.g., "CIFR-CA", "CIFR-FA", "CIFR-IA") +4. Copy each key and set the appropriate environment variable + +## ✅ Verification + +When you start the app, you'll see logging like: + +``` +🤖 CIFR Agent System - Initializing Agents +============================================================ +📋 Project ID: steel-pod-481123-e9 +📍 Location: us-central1 + +🔑 API Key Configuration: + Default Key: ✅ Set + Communication Agent (CA): ✅ Using dedicated key + Friction Detection (FA): ✅ Using dedicated key + Intervention Agent (IA): ✅ Using dedicated key +============================================================ + +[Communication Agent] Using dedicated API key (CA): AIzaSyAdX9HpiIJ-Z7... +[Friction Detection Agent] Using dedicated API key (FA): AIzaSyA-nv8nAyq8... +[Intervention Agent] Using dedicated API key (IA): AIzaSyDmyetZGSBcQ1... +``` + +## 🎯 Best Practices + +1. **Use Different Keys**: Use 3 different API keys for maximum quota distribution +2. **Monitor Usage**: Check [Google AI Studio Usage](https://ai.dev/usage?tab=rate-limit) regularly +3. **Rotate Keys**: If a key hits quota, you can rotate to a new key +4. **Enable Billing**: For production, enable billing for higher quotas + +## 🚨 Troubleshooting + +### If you still see quota errors: + +1. **Check Key Status**: Verify keys are active in Google AI Studio +2. **Wait for Reset**: Free tier quotas reset periodically +3. **Use More Keys**: Add additional keys for more quota +4. **Enable Billing**: Production use requires billing for higher limits + +### Current Status: + +Based on your `.env` file, you have: +- ✅ `GOOGLE_API_KEY_IA` set +- ⚠️ Need to set `GOOGLE_API_KEY_CA` and `GOOGLE_API_KEY_FA` for full per-agent setup + +## 📝 Example: Full Per-Agent Setup + +```bash +# Get 3 different API keys from Google AI Studio +KEY_1="your-first-api-key" +KEY_2="your-second-api-key" +KEY_3="your-third-api-key" + +# Start app with per-agent keys +GOOGLE_API_KEY_CA=$KEY_1 \ +GOOGLE_API_KEY_FA=$KEY_2 \ +GOOGLE_API_KEY_IA=$KEY_3 \ +python3 -c "from app import app; app.run(debug=True, host='127.0.0.1', port=9000)" +``` + +This distributes the API calls across 3 different keys, giving you 3x the free tier quota! 🚀 + diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000..c28605e04 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,62 @@ +# CIFR Agent System – Architecture + +Aligned with the official Agentic AI Hackathon template ([odsc2015/agentic-hackathon-template](https://github.com/odsc2015/agentic-hackathon-template)). + +## High-Level Overview +- Goal: Reduce intercompany friction via multi-agent collaboration. +- Gemini usage: `google.genai` for multimodal analysis, planning, and interventions. +- Delivery targets: CLI + Flask demo; cloud-ready with GCP services. + +## Component Diagram (ASCII) +``` +User Request (UI/CLI) + │ + ▼ +Planner (Gemini plan) + │ plan + ▼ +Executor ──────────────────────────────────────────────┐ + │ dispatch │ + ▼ │ +CommunicationAgent ──► KnowledgeAgent (context store) │ + │ │ │ + ▼ │ │ + FrictionDetectionAgent ◄──┘ │ + │ │ + ▼ │ + InterventionAgent → Response / Guidance to user │ +``` + +## Modules +- `src/planner.py`: Uses Gemini text to propose 3–6 JSON steps; heuristic fallback. +- `src/executor.py`: Iterates plan, calls agents, records trace. +- `src/memory.py`: Lightweight in-memory event log for demo traces. +- `cifr_agent_system/communication_agent.py`: Multimodal analysis (Gemini Vision/Text + Language API fallback). +- `cifr_agent_system/friction_detection_agent.py`: Misalignment detection (Gemini + heuristic keywords). +- `cifr_agent_system/intervention_agent.py`: Generates clarifications/action-items/mediation suggestions. +- `cifr_agent_system/knowledge_agent.py`: In-memory knowledge base (store/retrieve/search). +- `cifr_agent_system/config.py`: Env loading, optional insecure SSL for sandboxes, model IDs. + +## Data Flow +1) User goal → Planner → structured steps. +2) Executor loops steps: + - `analyze_messages`: CommunicationAgent performs multimodal analysis; stores context. + - `detect_friction`: FrictionDetectionAgent reasons over stored context. + - `generate_interventions`: InterventionAgent drafts guidance based on friction. +3) KnowledgeAgent stores analyses + interventions. +4) Executor returns results + trace to UI/CLI. + +## Environment & Secrets +- `.env` (gitignored): `GCP_PROJECT_ID`, `GCP_LOCATION`, `GOOGLE_API_KEY` (or per-agent keys), `GEMINI_PRO_MODEL_ID`, `GEMINI_PRO_VISION_MODEL_ID`. +- `key.json` should not be committed; rotate/remove if ever added. +- `ALLOW_INSECURE_SSL=1` only for sandbox environments. + +## Observability (current) +- Structured logging via `logging`; memory trace for demo. +- Flask API prints warnings for quota/heuristic fallbacks. + +## Extension Ideas +- Persist memory to Firestore/Spanner and add retrieval to planner prompts. +- Add tool router for calendars/search and external actions. +- Integrate OpenTelemetry tracing; promote heuristics to typed fallbacks with scores. + diff --git a/DEMO.md b/DEMO.md new file mode 100644 index 000000000..c09d926ff --- /dev/null +++ b/DEMO.md @@ -0,0 +1,21 @@ +# CIFR Agent System – Demo + +Provide a public 3–5 minute video link here (YouTube unlisted, Drive public, Loom, etc.). Submissions without a valid link will not be reviewed. + +## Video Link +- TODO: Paste final link here after recording. + +## Timestamped Highlights +- 00:00–00:30 Intro & setup +- 00:30–01:30 User input → Planning (Planner + Executor) +- 01:30–02:30 Tool calls & memory (CommunicationAgent, KnowledgeAgent, FrictionDetectionAgent) +- 02:30–03:30 Final output & edge cases (InterventionAgent + fallback flows) + +## How to Reproduce +1) `python -m venv .venv && source .venv/bin/activate` +2) `pip install -r cifr_agent_system/requirements.txt` +3) Create `.env` with `GCP_PROJECT_ID`, `GOOGLE_API_KEY`, optional `GEMINI_PRO_MODEL_ID`, `GEMINI_PRO_VISION_MODEL_ID`. +4) Run `python -m cifr_agent_system.main` for the CLI demo. +5) (Optional) Run web dashboard via option 2 in the menu. + + diff --git a/DEMO_GUIDE.md b/DEMO_GUIDE.md new file mode 100644 index 000000000..4047e710c --- /dev/null +++ b/DEMO_GUIDE.md @@ -0,0 +1,206 @@ +# 🚀 CIFR Agent System - Demo Guide + +## Overview +The **CIFR (Collaborative Intelligence & Friction Reduction) Agent System** is an intelligent orchestration platform that coordinates multiple specialized AI agents to monitor, analyze, and improve team collaboration. The system processes collaboration messages through a sophisticated pipeline where each agent plays a critical role in understanding context, detecting issues, and providing actionable insights. + +## 🎯 CIFR Orchestration System + +The CIFR system orchestrates four specialized AI agents in a coordinated workflow: + +**Pipeline Flow:** +``` +Input → Communication Agent → Knowledge Agent → Friction Detection Agent → Intervention Agent → Output +``` + +### How Orchestration Works: +1. **Input Processing:** Messages (text + optional images) enter the system +2. **Communication Analysis:** Communication Agent analyzes content using Gemini AI +3. **Context Storage:** Knowledge Agent stores analysis for future reference +4. **Friction Detection:** Friction Detection Agent identifies issues and patterns +5. **Intervention Generation:** Intervention Agent provides actionable solutions +6. **Output Delivery:** Comprehensive analysis and recommendations returned + +## 🤖 The Four AI Agents + +### 1. 💬 Communication Agent +**Role:** Primary message analyzer using Gemini AI +- Extracts sentiment scores and emotional tone +- Identifies key entities (people, projects, deadlines, etc.) +- Performs multimodal analysis (text + images) +- Provides deep context understanding + +**Technology:** Google Gemini AI (gemini-2.0-flash, gemini-2.5-flash) + +### 2. 📚 Knowledge Agent +**Role:** Central memory hub and context manager +- Stores all communication analyses in knowledge base +- Enables pattern recognition across conversations +- Maintains historical context for better understanding +- Supports cross-message analysis and trend detection + +**Key Feature:** Acts as the "memory" that allows agents to understand patterns over time + +### 3. ⚠️ Friction Detection Agent +**Role:** Conflict and misalignment identifier +- Detects friction points using AI reasoning +- Assesses severity levels (0.0 to 1.0) +- Identifies escalation patterns +- Recognizes recurring issues + +**Technology:** Google Gemini AI for intelligent reasoning + +### 4. 💡 Intervention Agent +**Role:** Solution provider and action generator +- Generates context-aware intervention suggestions +- Proposes clarification messages +- Creates actionable action items +- Suggests mediation strategies + +**Technology:** Google Gemini AI for intelligent recommendation generation + +## 🎯 Demo Examples + +### 1. 😤 Frustrated Delay Scenario +**Message:** "I am really frustrated with the constant delays on this feature. We've been waiting for 3 weeks now and the deadline is approaching fast. This is becoming a serious issue for our team and we need to accelerate immediately!" + +**What to Look For:** +- **Communication Agent:** Negative sentiment score (around -0.6), detects keywords like "frustrated", "delays", "issue" +- **Knowledge Agent:** Stores analysis in knowledge base for future pattern recognition +- **Friction Detection:** Should detect friction with high severity (0.6-0.8) +- **Intervention:** Suggests clarification or immediate action items + +**Demo Value:** Shows how the system identifies emotional friction and urgency, with Knowledge Agent maintaining context for future analysis. + +--- + +### 2. ⏰ Timeline Slippage Issue +**Message:** "The project timeline is slipping again. We were supposed to deliver Phase 2 by Friday, but now it looks like it will be delayed by at least another week. This is the third time we've had to push back deadlines. We need to have a serious discussion about resource allocation and priorities." + +**What to Look For:** +- **Communication Agent:** Negative sentiment, entities like "timeline", "deadline", "delays" +- **Knowledge Agent:** Stores recurring pattern data for trend analysis +- **Friction Detection:** Detects recurring issues and escalation patterns +- **Intervention:** Proposes action items for resource review and priority alignment + +**Demo Value:** Demonstrates pattern recognition and escalation detection, with Knowledge Agent tracking recurring issues. + +--- + +### 3. ✅ Positive Resolution +**Message:** "Great work on the latest sprint! The team really pulled together and we delivered everything on time. The new features are working perfectly and the client is very happy with the progress. Let's keep this momentum going!" + +**What to Look For:** +- **Communication Agent:** Positive sentiment score (around 0.7), positive keywords +- **Knowledge Agent:** Stores positive interaction for team health tracking +- **Friction Detection:** No friction detected +- **Intervention:** No intervention needed (confirms positive state) + +**Demo Value:** Shows the system correctly identifies positive communication and avoids false positives, with Knowledge Agent maintaining positive interaction history. + +--- + +### 4. ⚔️ Team Conflict +**Message:** "I completely disagree with the approach we're taking. The current design doesn't align with our original requirements and I think we're going in the wrong direction. We need to stop and reconsider before we waste more time and resources on this." + +**What to Look For:** +- **Communication Agent:** Negative sentiment, conflict indicators like "disagree", "wrong direction" +- **Knowledge Agent:** Stores conflict context for future reference and pattern tracking +- **Friction Detection:** High severity conflict detection +- **Intervention:** Suggests mediation or joint discussion to resolve differences + +**Demo Value:** Highlights conflict detection and mediation intervention capabilities, with Knowledge Agent maintaining conflict history. + +--- + +### 5. 📊 Data Analysis Request +**Message:** "This graph shows a significant drop in user engagement over the past month. We went from 85% active users to just 62%. The data suggests there might be an issue with the latest update. Can someone analyze this chart and help us understand what's happening?" + +**What to Look For:** +- **Communication Agent:** Multimodal analysis (if image uploaded), detects data-related entities +- **Knowledge Agent:** Stores data analysis context for trend tracking +- **Friction Detection:** Moderate concern detection +- **Intervention:** Suggests investigation or data review action items + +**Demo Value:** Demonstrates multimodal capabilities and data-driven friction detection, with Knowledge Agent maintaining data analysis history. + +--- + +## 🎨 Features Showcased + +### 1. **CIFR Orchestration System** +- **Coordinated Workflow:** Four specialized agents working in harmony +- **Pipeline Processing:** Sequential analysis from input to actionable output +- **Context Preservation:** Knowledge Agent maintains conversation history +- **Intelligent Routing:** Each agent processes and passes context to the next + +### 2. **Four AI Agents Working in Tandem** +- **Communication Agent:** Uses Gemini AI to analyze sentiment, extract entities, and understand context +- **Knowledge Agent:** Central memory hub storing and retrieving context for pattern recognition +- **Friction Detection Agent:** Identifies misalignments, conflicts, and friction points +- **Intervention Agent:** Provides actionable, intelligent suggestions + +### 3. **Gemini AI Integration** +- Real-time AI analysis of text content +- Multimodal support for images/charts +- Natural language understanding +- Context-aware responses +- Per-agent API key configuration for scalability + +### 4. **Knowledge Base & Context Management** +- Persistent storage of all communications +- Pattern recognition across conversations +- Historical context for better understanding +- Trend detection and escalation tracking + +### 5. **Intelligent Friction Detection** +- Sentiment analysis +- Pattern recognition +- Severity assessment +- Contextual understanding +- Recurring issue identification + +### 6. **Actionable Interventions** +- Clarification suggestions +- Action item proposals +- Mediation strategies +- Context-specific recommendations + +## 📝 How to Use the Demo + +1. **Visit:** http://127.0.0.1:9000 +2. **Click Demo Buttons:** Use the pre-built examples to see different scenarios +3. **Or Enter Custom Message:** Type your own collaboration message +4. **Upload Images (Optional):** Add charts, graphs, or diagrams for multimodal analysis +5. **View Results:** See all three agents' analysis in real-time + +## 🎯 Best Practices for Demo + +1. **Start with "Frustrated Delay"** - Most dramatic example showing full pipeline +2. **Show "Positive Resolution"** - Demonstrates accuracy (no false positives) +3. **Try "Team Conflict"** - Highlights mediation capabilities +4. **Use Custom Messages** - Show real-time analysis of audience input +5. **Upload Images** - Demonstrate multimodal capabilities if time permits + +## 🔍 What Makes This Demo Compelling + +- **Real-World Scenarios:** Examples based on actual collaboration challenges +- **AI-Powered Intelligence:** Shows advanced Gemini AI capabilities +- **Complete Pipeline:** Demonstrates end-to-end agent workflow +- **Actionable Insights:** Provides real value, not just analysis +- **Professional UI:** Modern, intuitive interface + +## 💡 Key Talking Points + +1. **CIFR Orchestration System:** Intelligent coordination of four specialized agents in a seamless pipeline +2. **Multi-Agent Architecture:** Four specialized AI agents (Communication, Knowledge, Friction Detection, Intervention) working together +3. **Knowledge Base Intelligence:** Central memory hub enabling pattern recognition and historical context +4. **Gemini AI Integration:** State-of-the-art language understanding with per-agent API key configuration +5. **Proactive Intervention:** Not just detection, but actionable solutions tailored to context +6. **Multimodal Analysis:** Handles text, images, and data visualizations +7. **Real-Time Processing:** Fast, responsive analysis through coordinated agent workflow +8. **Context-Aware:** Knowledge Agent maintains conversation history for better understanding over time + +--- + +**Ready to demo?** Visit http://127.0.0.1:9000 and click any demo button to get started! 🚀 + diff --git a/EXPLANATION.md b/EXPLANATION.md new file mode 100644 index 000000000..3b6796c8c --- /dev/null +++ b/EXPLANATION.md @@ -0,0 +1,41 @@ +# CIFR Agent System – Explanation + +This document follows the Agentic AI Hackathon template requirements. + +## Planning Style +- Planner uses Gemini text model (`google.genai`) to turn user goals into 3–6 sub-tasks. +- Output format: JSON list with `id`, `action`, `input`, `notes`, `expected_output`. +- Fallback heuristics ensure planning works even if Gemini is unavailable. + +## Execution Flow +1) Planner creates steps. +2) Executor iterates steps and routes to agents: + - `analyze_messages` → CommunicationAgent (Gemini Vision/Text + Language API). + - `detect_friction` → FrictionDetectionAgent (Gemini reasoning). + - `generate_interventions` → InterventionAgent (Gemini suggestions). +3) Results and reasoning traces are stored in KnowledgeAgent and Memory store. + +## Memory Usage +- `KnowledgeAgent` maintains contextual store for analyses and recommendations. +- `src/memory.py` provides a lightweight append-only log for executions. +- Future: persist to datastore and add retrieval-augmented prompts. + +## Tool Integration +- Gemini API via `google.genai` for: + - Multimodal analysis (CommunicationAgent). + - Friction reasoning (FrictionDetectionAgent). + - Intervention drafting (InterventionAgent). + - Planning (Planner). +- GCP language services for sentiment/entities as a fallback/augmenter. + +## Limitations +- Requires valid `GOOGLE_API_KEY`; Config raises if missing. +- Memory is in-process only; no persistence yet. +- Planning/exec parsing assumes well-formed Gemini JSON; guarded with fallbacks. +- Demo uses synthetic messages; real integrations (Slack/Gmail/Drive) are stubs. + +## Known Risks +- `key.json` is present locally; remove from git history and rely on `.env`. +- Network/API failures degrade to heuristic flows; add retries/backoff for prod. + + diff --git a/FALLBACK_SERVICE_SETUP.md b/FALLBACK_SERVICE_SETUP.md new file mode 100644 index 000000000..f6d994468 --- /dev/null +++ b/FALLBACK_SERVICE_SETUP.md @@ -0,0 +1,128 @@ +# 🔄 OpenAI-Compatible Fallback Service Setup + +## Overview + +The CIFR Agent System now includes an **optional fallback mechanism** that automatically switches to an OpenAI-compatible service when the free tier Google Gemini API hits quota limits (429 errors). + +**⚠️ Important for Hackathon:** +- **Primary Service:** Free tier Google Gemini API (for hackathon judges) +- **Fallback Service:** OpenAI-compatible service (demo/backup only - NOT available to hackathon judges) +- The system will **always try the free tier first** - fallback is only used when quota is exhausted + +## How It Works + +1. **Primary:** System tries Google Gemini API (free tier) +2. **On 429 Error:** If quota exhausted, automatically tries OpenAI-compatible fallback +3. **If Both Fail:** Falls back to heuristic analysis (for hackathon demo) + +## Environment Variables + +Add these to your `.env` file or set as environment variables: + +```env +# Enable fallback service (set to "1" to enable, "0" to disable) +ENABLE_OPENAI_FALLBACK=1 + +# OpenAI-compatible service base URL +OPENAI_FALLBACK_BASE_URL=https://caas-gocode-prod.caas-prod.prod.onkatana.net/v1 + +# OpenAI-compatible API key +OPENAI_FALLBACK_API_KEY=sk-k5xL1aEj4Vi0YPPJ3pdxMw + +# Model name on the fallback service (must match available models) +OPENAI_FALLBACK_MODEL=gemini-2.0-flash-001 +``` + +## Setup Instructions + +### Option 1: Add to .env File + +Add these lines to your `.env` file: + +```env +ENABLE_OPENAI_FALLBACK=1 +OPENAI_FALLBACK_BASE_URL=https://caas-gocode-prod.caas-prod.prod.onkatana.net/v1 +OPENAI_FALLBACK_API_KEY=sk-k5xL1aEj4Vi0YPPJ3pdxMw +OPENAI_FALLBACK_MODEL=gemini-2.0-flash-001 +``` + +### Option 2: Set as Environment Variables + +When starting the Flask app: + +```bash +ENABLE_OPENAI_FALLBACK=1 \ +OPENAI_FALLBACK_BASE_URL='https://caas-gocode-prod.caas-prod.prod.onkatana.net/v1' \ +OPENAI_FALLBACK_API_KEY='sk-k5xL1aEj4Vi0YPPJ3pdxMw' \ +OPENAI_FALLBACK_MODEL='gemini-2.0-flash-001' \ +python3 -c "from app import app; app.run(debug=True, host='127.0.0.1', port=9000)" +``` + +## Available Models on Fallback Service + +The fallback service supports these Gemini models: +- `gemini-2.0-flash-001` ✅ (Recommended) +- `gemini-2.0-flash-exp` +- `gemini-2.5-flash` +- `gemini-2.5-flash-image` +- `gemini-2.5-pro` +- And more... + +## Behavior + +### When Fallback is Enabled (`ENABLE_OPENAI_FALLBACK=1`): + +1. **Free Tier Works:** Uses Google Gemini API (primary) +2. **Free Tier Exhausted (429):** Automatically switches to OpenAI-compatible service +3. **Both Fail:** Uses heuristic analysis (for hackathon demo) + +### When Fallback is Disabled (`ENABLE_OPENAI_FALLBACK=0` or not set): + +1. **Free Tier Works:** Uses Google Gemini API +2. **Free Tier Exhausted (429):** Uses heuristic analysis (for hackathon demo) + +## For Hackathon Judges + +**Important:** Hackathon judges will **NOT** have access to the fallback service. The system is designed to: + +1. **Primary:** Use free tier Google Gemini API (available to all) +2. **Fallback:** Use heuristic analysis when API quota is exhausted +3. **Optional:** Fallback service is only for your personal demo/backup + +The system will work perfectly for hackathon judges using only the free tier API and heuristic fallback. + +## Testing + +To test if the fallback is working: + +1. Set `ENABLE_OPENAI_FALLBACK=1` +2. Use a Google Gemini API key that's exhausted (429 error) +3. Make an API call - it should automatically use the fallback service +4. Check logs for: "Using OpenAI-compatible fallback service" + +## Troubleshooting + +### Fallback Not Working? + +1. **Check if enabled:** `ENABLE_OPENAI_FALLBACK=1` +2. **Check base URL:** Must be correct OpenAI-compatible endpoint +3. **Check API key:** Must be valid for the service +4. **Check model name:** Must match available models on the service +5. **Check logs:** Look for error messages in console + +### Service Not Available? + +If the fallback service is not accessible: +- System will automatically use heuristic analysis +- Hackathon demo will still work +- No action needed - this is expected behavior + +## Summary + +- ✅ **Primary:** Free tier Google Gemini API (for hackathon) +- ✅ **Fallback:** OpenAI-compatible service (demo/backup only) +- ✅ **Final Fallback:** Heuristic analysis (always works) +- ⚠️ **Hackathon Judges:** Will only use primary + heuristic (no fallback service access) + +The system is designed to work perfectly for hackathon judges while providing you with a backup option for demos! 🚀 + diff --git a/README.md b/README.md new file mode 100644 index 000000000..fdd224ef4 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# CIFR Agent System (Agentic AI Hackathon) + +This repo aligns with the [Agentic AI App Hackathon template](https://github.com/odsc2015/agentic-hackathon-template) and implements a multi-agent workflow for communication analysis, friction detection, and intervention suggestions using Google Gemini. + +## Contents +- `src/`: hackathon-required planner/executor/memory modules. +- `cifr_agent_system/`: domain agents (communication, friction detection, intervention, knowledge, utils, config). +- `frontend/`: lightweight Flask UI for demo. +- Docs: `ARCHITECTURE.md`, `EXPLANATION.md`, `DEMO.md`, `API_KEY_SETUP.md`. + +## Prerequisites +- Python 3.10+ recommended (works on 3.9 with shims, but prefer 3.10+). +- Google Gemini API key from Google AI Studio. +- GCP Project ID if using Cloud Language fallback. + +## Setup +```bash +python -m venv .venv && source .venv/bin/activate +pip install -r cifr_agent_system/requirements.txt +``` + +Create a `.env` (not committed) with: +``` +GCP_PROJECT_ID= +GCP_LOCATION=us-central1 +GOOGLE_API_KEY= +# Optional per-agent overrides +GOOGLE_API_KEY_CA= +GOOGLE_API_KEY_FA= +GOOGLE_API_KEY_IA= +GEMINI_PRO_MODEL_ID=gemini-2.0-flash +GEMINI_PRO_VISION_MODEL_ID=gemini-2.5-flash +``` + +## Running +- CLI demo: `python -m cifr_agent_system.main` +- Flask UI: `python app.py` then open `http://localhost:5000` +- Planner/Executor programmatic use: +```python +from src.executor import Executor +from src.memory import MemoryStore +# instantiate agents from cifr_agent_system.* +``` + +## Hackathon Checklist (per template) +- Fork named after team/participant. +- Agent built under `src/` (planner/executor/memory present). +- Gemini API integrated (`google.genai` in planner + agents). +- Docs filled: `README.md`, `ARCHITECTURE.md`, `EXPLANATION.md`, `DEMO.md` (video link pending). +- Video demo to be recorded and linked in `DEMO.md`. + +## Security & Secrets +- Do **not** commit `.env` or `key.json`. They are gitignored. Rotate any previously committed keys. +- Set `ALLOW_INSECURE_SSL=1` only in constrained sandboxes; keep unset for real deployments. + +## Troubleshooting +- If Flask complains about `.env` permissions, set `FLASK_SKIP_DOTENV=1` (already defaulted in `app.py`). +- If Gemini quota is hit, agents fall back to heuristic logic and log warnings. + +## License +Apache-2.0 (template baseline). + diff --git a/app.py b/app.py new file mode 100644 index 000000000..531867d52 --- /dev/null +++ b/app.py @@ -0,0 +1,259 @@ +import logging +import os +from datetime import datetime +from flask import Flask, request, jsonify, render_template +from dotenv import load_dotenv +import base64 +import json + +# Flask CLI tries to auto-load .env and can crash if the file isn't readable. +# Prevent that and rely on our explicit load_dotenv below. +os.environ.setdefault("FLASK_SKIP_DOTENV", "1") + +# Ensure environment variables are loaded for Config to access them. +# If the .env file is not readable (e.g., permissions or sandbox filters), +# fall back to existing environment variables. +try: + load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) +except PermissionError: + print("Warning: .env not readable; relying on existing environment variables.") + +# Import agents and Config from your cifr_agent_system package +from cifr_agent_system.config import Config +from cifr_agent_system.communication_agent import CommunicationAgent +from cifr_agent_system.knowledge_agent import KnowledgeAgent +from cifr_agent_system.friction_detection_agent import FrictionDetectionAgent +from cifr_agent_system.intervention_agent import InterventionAgent +from cifr_agent_system.utils import generate_unique_id + + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +def serialize_google_cloud_object(obj): + """Recursively converts Google Cloud client library objects to JSON serializable types.""" + # Handle dicts first (before checking __dict__) + if isinstance(obj, dict): + return {k: serialize_google_cloud_object(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + # Recursively process lists/tuples + return [serialize_google_cloud_object(elem) for elem in obj] + elif isinstance(obj, datetime): + return obj.isoformat() + elif hasattr(obj, '__dict__'): + # Handle Google Cloud protobuf objects + if hasattr(obj, '_pb'): + return serialize_google_cloud_object(obj._pb) + # Recursively process dictionary-like objects + return {k: serialize_google_cloud_object(v) for k, v in obj.__dict__.items() if not k.startswith('_')} + # Handle specific Google Cloud client objects like Sentiment.score, Sentiment.magnitude, and Entities + elif hasattr(obj, 'score') and hasattr(obj, 'magnitude'): + return {"score": obj.score, "magnitude": obj.magnitude} + elif hasattr(obj, 'entities') and isinstance(obj.entities, (list, tuple)): + return [serialize_google_cloud_object(entity) for entity in obj.entities] + elif hasattr(obj, 'name') and hasattr(obj, 'type_') and hasattr(obj, 'salience'): + return {"name": obj.name, "type": str(obj.type_), "salience": obj.salience} + elif hasattr(obj, 'name') and hasattr(obj, 'confidence'): + return {"name": obj.name, "confidence": obj.confidence} + # Handle generic protobuf messages by converting to a dictionary + elif hasattr(obj, 'DESCRIPTOR') and hasattr(obj, 'ListFields'): + return {field.name: serialize_google_cloud_object(getattr(obj, field.name)) for field, _ in obj.ListFields()} + # Base cases for primitive types + elif isinstance(obj, (int, float, str, bool, type(None))): + return obj + # For any other object type, try to stringify or represent as dict + else: + try: + # Try to convert to dict if it has to_dict or similar method + if hasattr(obj, 'to_dict'): + return obj.to_dict() + # Or just convert to string representation for unknown complex objects + return str(obj) + except Exception: + return f"" + +app = Flask(__name__, + static_folder='./frontend/static', + template_folder='./frontend/templates') + +# Initialize agents globally or per-request if state management is complex +# For simplicity, we'll initialize them globally here. +logger.info("🤖 CIFR Agent System - Initializing Agents") +logger.info("📋 Project ID: %s", Config.GCP_PROJECT_ID) +logger.info("📍 Location: %s", Config.GCP_LOCATION) +logger.info( + "🔑 API Key Configuration: default=%s, CA=%s, FA=%s, IA=%s", + "set" if Config.GOOGLE_API_KEY else "missing", + "dedicated" if Config.GOOGLE_API_KEY_CA else "default", + "dedicated" if Config.GOOGLE_API_KEY_FA else "default", + "dedicated" if Config.GOOGLE_API_KEY_IA else "default", +) + +knowledge_agent = KnowledgeAgent(project_id=Config.GCP_PROJECT_ID, location=Config.GCP_LOCATION) +communication_agent = CommunicationAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) +friction_detection_agent = FrictionDetectionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, location=Config.GCP_LOCATION) +intervention_agent = InterventionAgent(project_id=Config.GCP_PROJECT_ID, knowledge_agent=knowledge_agent, friction_detection_agent=friction_detection_agent, location=Config.GCP_LOCATION) + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/api/process_message', methods=['POST']) +def process_message_api(): + data = request.form + text_content = data.get('text_content', '') + image_file = request.files.get('image_file') + + image_bytes = None + if image_file: + image_bytes = image_file.read() + + message_id = generate_unique_id("web_message") + timestamp = datetime.now().isoformat() + + sample_message = { + "message_id": message_id, + "text_content": text_content, + "image_bytes": image_bytes, + "timestamp": timestamp, + "sender": "Web User" + } + + logger.info("[API] Processing message ID: %s", message_id) + + results = { + "original_message": sample_message, + "communication_analysis": None, + "knowledge_update_status": None, + "friction_detection": None, + "intervention_suggestion": None, + "error": None, + "warnings": [] + } + + try: + # 1. Communication Agent processing + comm_agent_results = communication_agent.process_collaboration_message(sample_message) + logger.debug("Communication agent results type: %s", type(comm_agent_results)) + # Ensure proper JSON serialization - always return a dict, never a string + comm_serialized = serialize_google_cloud_object(comm_agent_results) + logger.debug("After serialization, type: %s, is dict: %s", type(comm_serialized), isinstance(comm_serialized, dict)) + # Ensure it's a dict, not a string + if isinstance(comm_serialized, str): + logger.warning("Communication analysis was serialized as string! Attempting to parse...") + try: + comm_serialized = json.loads(comm_serialized) + except: + logger.warning("JSON parse failed, trying Python literal eval") + import ast + try: + comm_serialized = ast.literal_eval(comm_serialized) + except Exception as e: + logger.error("Failed to parse communication analysis: %s", e) + comm_serialized = {"error": "Failed to parse communication analysis", "raw": comm_serialized[:200]} + if not isinstance(comm_serialized, dict): + logger.error("Communication analysis is not a dict after processing! Type: %s", type(comm_serialized)) + comm_serialized = {"error": "Invalid response format", "type": str(type(comm_serialized))} + results["communication_analysis"] = comm_serialized + results["knowledge_update_status"] = "Context stored under 'communication_analysis_{}'".format(message_id) + + # Check for quota errors and API source in communication analysis + comm_str = json.dumps(comm_serialized) if isinstance(comm_serialized, dict) else str(comm_agent_results) + if "429" in comm_str or "RESOURCE_EXHAUSTED" in comm_str or "quota" in comm_str.lower(): + results["warnings"].append("Communication Agent: Gemini API quota exceeded. Using fallback service.") + # Check if using fallback service + analysis = comm_serialized.get("analysis", {}) if isinstance(comm_serialized, dict) else {} + if isinstance(analysis, dict) and analysis.get("api_source") == "fallback": + results["warnings"].append("Communication Agent: Using OpenAI-compatible fallback service (Gemini quota exhausted).") + + # 2. Friction Detection + friction_results = friction_detection_agent.detect_communication_friction(f"communication_analysis_{message_id}") + logger.debug("Friction detection results type: %s", type(friction_results)) + # Ensure proper JSON serialization - always return a dict, never a string + friction_serialized = serialize_google_cloud_object(friction_results) + logger.debug("After serialization, type: %s, is dict: %s", type(friction_serialized), isinstance(friction_serialized, dict)) + # Ensure it's a dict, not a string + if isinstance(friction_serialized, str): + logger.warning("Friction detection was serialized as string! Attempting to parse...") + try: + friction_serialized = json.loads(friction_serialized) + except: + logger.warning("JSON parse failed, trying Python literal eval") + import ast + try: + friction_serialized = ast.literal_eval(friction_serialized) + except Exception as e: + logger.error("Failed to parse friction detection: %s", e) + friction_serialized = {"error": "Failed to parse friction detection", "raw": friction_serialized[:200]} + if not isinstance(friction_serialized, dict): + logger.error("Friction detection is not a dict after processing! Type: %s", type(friction_serialized)) + friction_serialized = {"error": "Invalid response format", "type": str(type(friction_serialized))} + results["friction_detection"] = friction_serialized + + # Check for quota errors and API source in friction detection + friction_str = json.dumps(friction_serialized) if isinstance(friction_serialized, dict) else str(friction_results) + if "429" in friction_str or "RESOURCE_EXHAUSTED" in friction_str or "quota" in friction_str.lower(): + results["warnings"].append("Friction Detection Agent: Gemini API quota exceeded. Using fallback service.") + # Check if using fallback service + if isinstance(friction_serialized, dict) and friction_serialized.get("api_source") == "fallback": + results["warnings"].append("Friction Detection Agent: Using OpenAI-compatible fallback service (Gemini quota exhausted).") + + # 3. Intervention Suggestion + intervention_suggestion = intervention_agent.suggest_intervention(f"communication_analysis_{message_id}") + logger.debug("Intervention suggestion results type: %s", type(intervention_suggestion)) + # Ensure proper JSON serialization - always return a dict, never a string + intervention_serialized = serialize_google_cloud_object(intervention_suggestion) + logger.debug("After serialization, type: %s, is dict: %s", type(intervention_serialized), isinstance(intervention_serialized, dict)) + # Ensure it's a dict, not a string + if isinstance(intervention_serialized, str): + logger.warning("Intervention suggestion was serialized as string! Attempting to parse...") + try: + intervention_serialized = json.loads(intervention_serialized) + except: + logger.warning("JSON parse failed, trying Python literal eval") + import ast + try: + intervention_serialized = ast.literal_eval(intervention_serialized) + except Exception as e: + logger.error("Failed to parse intervention suggestion: %s", e) + intervention_serialized = {"error": "Failed to parse intervention suggestion", "raw": intervention_serialized[:200]} + if not isinstance(intervention_serialized, dict): + logger.error("Intervention suggestion is not a dict after processing! Type: %s", type(intervention_serialized)) + intervention_serialized = {"error": "Invalid response format", "type": str(type(intervention_serialized))} + results["intervention_suggestion"] = intervention_serialized + + # Check for quota errors in intervention + intervention_str = json.dumps(intervention_serialized) if isinstance(intervention_serialized, dict) else str(intervention_suggestion) + if "429" in intervention_str or "RESOURCE_EXHAUSTED" in intervention_str or "quota" in intervention_str.lower(): + results["warnings"].append("Intervention Agent: Gemini API quota exceeded. Using fallback service.") + + except Exception as e: + results["error"] = str(e) + logger.exception("[API Error] %s", e) + + # Ensure all responses are JSON-serializable + try: + # Test serialization + json.dumps(results) + except (TypeError, ValueError) as e: + logger.warning("Response contains non-serializable objects, attempting to fix: %s", e) + # Convert any remaining non-serializable objects to strings + def make_serializable(obj): + if isinstance(obj, dict): + return {k: make_serializable(v) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + return [make_serializable(item) for item in obj] + elif isinstance(obj, (str, int, float, bool, type(None))): + return obj + else: + return str(obj) + results = make_serializable(results) + + return jsonify(results) + +if __name__ == '__main__': + # It's recommended to run Flask in development mode for easier debugging. + # For production, use a production-ready WSGI server like Gunicorn or uWSGI. + app.run(debug=True, host='0.0.0.0', port=5000, load_dotenv=False) + diff --git a/cifr_agent_system/config.py b/cifr_agent_system/config.py new file mode 100644 index 000000000..666c565c8 --- /dev/null +++ b/cifr_agent_system/config.py @@ -0,0 +1,67 @@ +import os +import ssl +from dotenv import load_dotenv + +# Allow opting into insecure SSL only when explicitly requested (for sandboxes). +ALLOW_INSECURE_SSL = os.getenv("ALLOW_INSECURE_SSL", "0") == "1" + +if ALLOW_INSECURE_SSL: + def _no_verify_context(*args, **kwargs): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + return ctx + + ssl._create_default_https_context = _no_verify_context + ssl.create_default_context = _no_verify_context + ssl._create_unverified_context = _no_verify_context + +# Ensure SSL_CERT_FILE points to a readable cert bundle (to avoid permission errors +# when the default system cert store is blocked in sandboxed environments). +try: + import certifi + cert_path = certifi.where() + os.environ["SSL_CERT_FILE"] = cert_path + os.environ["REQUESTS_CA_BUNDLE"] = cert_path + os.environ["GRPC_DEFAULT_SSL_ROOTS_FILE_PATH"] = cert_path +except Exception: + pass + +# Load environment variables from .env file; if unreadable (permissions/sandbox), fall back to existing env. +try: + load_dotenv() +except PermissionError: + print("Warning: cifr_agent_system/.env not readable; relying on existing environment variables.") + +class Config: + # GCP Project Settings + GCP_PROJECT_ID = os.getenv("GCP_PROJECT_ID") + GCP_LOCATION = os.getenv("GCP_LOCATION", "us-central1") # Default to us-central1 + GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") # Legacy single key / default + GOOGLE_API_KEY_CA = os.getenv("GOOGLE_API_KEY_CA") # Communication Agent + GOOGLE_API_KEY_FA = os.getenv("GOOGLE_API_KEY_FA") # Friction Detection Agent + GOOGLE_API_KEY_IA = os.getenv("GOOGLE_API_KEY_IA") # Intervention Agent + + # Vertex AI / Gemini Settings + VERTEX_AI_ENDPOINT = f"{GCP_LOCATION}-aiplatform.googleapis.com" + GEMINI_PRO_VISION_MODEL_ID = os.getenv("GEMINI_PRO_VISION_MODEL_ID", "gemini-2.5-flash") + GEMINI_PRO_MODEL_ID = os.getenv("GEMINI_PRO_MODEL_ID", "gemini-2.0-flash") + + # HTTP options for google.genai; disable SSL verification in constrained environments. + GENAI_HTTP_OPTIONS = {"verify": False} + + # OpenAI-Compatible Fallback Service (Secondary - for demo/backup only) + # NOTE: Hackathon judges won't have access to this service + # This is only used as fallback when free tier Google Gemini API hits quota limits + OPENAI_FALLBACK_BASE_URL = os.getenv("OPENAI_FALLBACK_BASE_URL") # e.g., https://caas-gocode-prod.caas-prod.prod.onkatana.net/v1 + OPENAI_FALLBACK_API_KEY = os.getenv("OPENAI_FALLBACK_API_KEY") # OpenAI-compatible API key + OPENAI_FALLBACK_MODEL = os.getenv("OPENAI_FALLBACK_MODEL", "gemini-2.0-flash-001") # Model name on fallback service + ENABLE_OPENAI_FALLBACK = os.getenv("ENABLE_OPENAI_FALLBACK", "0") == "1" # Set to "1" to enable fallback + + # Ensure required environment variables are set + if not GCP_PROJECT_ID: + raise ValueError("GCP_PROJECT_ID environment variable not set.") + if not (GOOGLE_API_KEY or GOOGLE_API_KEY_CA or GOOGLE_API_KEY_FA or GOOGLE_API_KEY_IA): + raise ValueError("No Gemini API key found. Set GOOGLE_API_KEY or per-agent keys GOOGLE_API_KEY_CA/FA/IA. Get your API key from https://aistudio.google.com/app/apikey") + + # Add other configuration variables as needed (e.g., database settings) diff --git a/cifr_agent_system/project_idea.txt b/cifr_agent_system/project_idea.txt new file mode 100644 index 000000000..d3eb465f3 --- /dev/null +++ b/cifr_agent_system/project_idea.txt @@ -0,0 +1,51 @@ +Project Idea: Collaborative Intelligence & Friction Reduction (CIFR) Agent System +1. Concept: +The CIFR Agent System acts as an intelligent intermediary and facilitator for intercompany projects. It uses a network of specialized AI agents to actively monitor communication channels (emails, chat, documents), identify potential friction points (e.g., conflicting statements, stalled decisions, knowledge gaps), and proactively intervene with intelligent suggestions, clarifications, or even by initiating automated actions. The goal is to ensure smoother workflows, clearer understanding, and accelerated progress in complex multi-party collaborations. +2. Multi-Agent System: +Communication & Sentiment Analysis Agent: +Role: Monitors intercompany communication channels. +Functionality: Ingests and analyzes communication data (e.g., shared documents, email threads, chat logs from platforms like Google Chat, Slack). Uses Gemini's advanced NLP capabilities to understand context, extract key decisions, identify action items, and perform sentiment analysis. It flags communication breakdowns, misunderstandings, or rising tensions based on linguistic cues. +Multimodal Aspect: Could analyze images or diagrams shared in documents to extract context (e.g., flowcharts indicating processes, architectural diagrams). Gemini's ability to interpret image content in conjunction with text would be crucial here. +Knowledge & Context Integration Agent: +Role: Builds and maintains a unified understanding of the project, shared knowledge, and historical context. +Functionality: Aggregates information from all communication channels and shared repositories (e.g., Google Drive, Confluence, internal knowledge bases). Uses Gemini's reasoning capabilities to identify implicit dependencies, potential overlaps, or conflicting information across different sources. It can also "learn" about each company's specific terminology, processes, and priorities. +Reasoning/Intellisearch: Employs intellisearch to find relevant past discussions, decisions, or documents when a new query arises or a friction point is detected. It reasons about which pieces of information are most pertinent to resolve a given issue. +Expectation Alignment & Conflict Detection Agent: +Role: Identifies misaligned expectations and potential conflicts before they escalate. +Functionality: By comparing statements, action items, and goals from different collaborating parties, this agent uses Gemini's reasoning to detect discrepancies. For instance, if Company A states a deadline that conflicts with Company B's projected resource availability, this agent flags it. It can also identify subtle conflicts of interest or resource contention. +Agentic Protocols: Could trigger a "Mediation Sub-Agent" or suggest a structured discussion to resolve the conflict, providing all necessary context. +Decision & Action Facilitation Agent: +Role: Accelerates decision-making and ensures accountability for action items. +Functionality: Tracks decisions made, assigns action items, and monitors progress. If a decision is stalled, or an action item is overdue, this agent uses Gemini to gently nudge relevant parties, provide a summary of the outstanding issue, or suggest potential next steps to unblock the process. +Multimodal Aspect: Could generate simple visual summaries of project status, decision trees, or dependency graphs to aid clarity. +Intervention & Suggestion Agent: +Role: Provides proactive assistance and suggestions to reduce friction. +Functionality: Based on the insights from other agents, this agent provides context-aware suggestions. Examples: +"It seems there's a misunderstanding regarding the user authentication module. Here's a summary of the last discussion and the agreed-upon API contract." (Referencing information from the Knowledge Agent). +"The deadline for feature X from Company B seems tight given their current workload. Would you like me to suggest a revised timeline or explore resource sharing options?" (Based on Expectation Alignment Agent's findings). +"Team A is blocked waiting for the data schema from Team B. Here's the relevant documentation and a direct link to their contact." +Reasoning/Automated Reasoning: This agent heavily relies on the reasoning capabilities to understand the friction point, identify the best intervention strategy, and formulate a helpful, context-rich response. +3. Google Cloud, Data Science, & Engineering Team Perspective: +GCP Services: +Communication Ingestion: Cloud Pub/Sub (for real-time chat/email updates), Cloud Storage (for shared documents, historical data). +NLP & Multimodal AI: Vertex AI (for Gemini integration, custom NLP models, fine-tuning for domain-specific terminology), Cloud Natural Language API (for sentiment analysis, entity extraction). +Knowledge Graph/Database: Neo4j on GCP Marketplace, or a custom solution on Cloud Spanner/Firestore for storing interconnected knowledge and relationships between entities. +Orchestration: Cloud Composer (Apache Airflow) for managing complex, event-driven agent workflows. +Deployment: Cloud Run (for serverless microservices for each agent), Google Kubernetes Engine (for more complex, stateful agents). +Security & Compliance: Cloud DLP (for sensitive information detection), Identity and Access Management (IAM) for secure cross-company data access control. +Data Science Focus: Advanced NLP, sentiment analysis, topic modeling, knowledge graph construction, anomaly detection (for communication patterns), predictive modeling (for potential friction points), and active learning for agent improvement. +Engineering Teams: +Microservices Architecture: Encourages a clean, modular design with independent agents. +API-First Approach: Agents communicate via well-defined APIs. +Observability: Comprehensive logging, tracing, and monitoring (Cloud Monitoring, Cloud Logging, Cloud Trace) to understand agent behavior and system health. +Scalability: Leverages GCP's auto-scaling capabilities for various services. +Interoperability: Design for easy integration with existing communication and document management tools used by collaborating companies. +4. Multimodal and Reasoning: +Multimodal: Gemini's ability to interpret not just text but also diagrams, screenshots of UI, or even short video clips within shared documents is vital for a holistic understanding of project context. For instance, if an engineering team shares a screenshot of an error, Gemini could analyze the image and the surrounding text to understand the problem. +Reasoning/Automated Reasoning: The core of this system is the agents' ability to reason. They need to: +Understand nuances in human communication. +Synthesize information from disparate sources. +Identify causal links between actions and outcomes. +Anticipate potential problems and suggest preventive measures. +Formulate intelligent, context-aware interventions to reduce friction. +This CIFR Agent System addresses a real-world business challenge, provides ample opportunity to showcase advanced AI capabilities (especially with Gemini's multimodal and reasoning power), and offers a compelling narrative for a hackathon. diff --git a/cifr_agent_system/requirements.txt b/cifr_agent_system/requirements.txt new file mode 100644 index 000000000..75683a146 --- /dev/null +++ b/cifr_agent_system/requirements.txt @@ -0,0 +1,12 @@ +google-cloud-aiplatform +google-cloud-dlp +google-cloud-pubsub +google-cloud-storage +google-cloud-language +google-genai # Added for direct Gemini API access +openai # For OpenAI-compatible fallback service (optional) +cryptography +pillow +python-dotenv # For local environment variable management +Flask[async] +Flask[async] # For the web UI diff --git a/test_api_keys.py b/test_api_keys.py new file mode 100644 index 000000000..1140aaf54 --- /dev/null +++ b/test_api_keys.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Test script to check which API keys have exhausted their quota""" + +import os +import sys +import google.genai as genai +from dotenv import load_dotenv + +# Load environment variables +try: + load_dotenv() +except: + pass + +# Import config +sys.path.insert(0, os.path.dirname(__file__)) +from cifr_agent_system.config import Config + +def test_api_key(key_name, api_key, model="gemini-2.0-flash"): + """Test a single API key""" + if not api_key: + return {"status": "not_set", "key_name": key_name} + + try: + client = genai.Client(api_key=api_key) + response = client.models.generate_content( + model=model, + contents=[{"parts": [{"text": "Hello, this is a test."}]}] + ) + + if response.candidates: + return { + "status": "working", + "key_name": key_name, + "key_preview": api_key[:20] + "...", + "response": response.candidates[0].content.parts[0].text[:50] + "..." + } + else: + return {"status": "no_response", "key_name": key_name} + + except Exception as e: + error_str = str(e) + if "429" in error_str or "RESOURCE_EXHAUSTED" in error_str or "quota" in error_str.lower(): + return { + "status": "quota_exhausted", + "key_name": key_name, + "key_preview": api_key[:20] + "...", + "error": "Quota exceeded - 429 RESOURCE_EXHAUSTED" + } + else: + return { + "status": "error", + "key_name": key_name, + "key_preview": api_key[:20] + "...", + "error": str(e)[:100] + } + +def main(): + print("="*70) + print("🔍 CIFR Agent System - API Key Quota Diagnostic") + print("="*70) + print() + + # Test all keys + keys_to_test = [ + ("Default Key (GOOGLE_API_KEY)", Config.GOOGLE_API_KEY), + ("Communication Agent (GOOGLE_API_KEY_CA)", Config.GOOGLE_API_KEY_CA), + ("Friction Detection Agent (GOOGLE_API_KEY_FA)", Config.GOOGLE_API_KEY_FA), + ("Intervention Agent (GOOGLE_API_KEY_IA)", Config.GOOGLE_API_KEY_IA), + ] + + results = [] + for key_name, api_key in keys_to_test: + print(f"Testing {key_name}...", end=" ", flush=True) + result = test_api_key(key_name, api_key) + results.append(result) + + if result["status"] == "working": + print("✅ WORKING") + elif result["status"] == "quota_exhausted": + print("❌ QUOTA EXHAUSTED") + elif result["status"] == "not_set": + print("⚠️ NOT SET") + elif result["status"] == "error": + print(f"❌ ERROR: {result.get('error', 'Unknown error')[:50]}") + else: + print(f"⚠️ {result['status'].upper()}") + + print() + print("="*70) + print("📊 Summary Report") + print("="*70) + print() + + working_keys = [r for r in results if r["status"] == "working"] + exhausted_keys = [r for r in results if r["status"] == "quota_exhausted"] + not_set_keys = [r for r in results if r["status"] == "not_set"] + error_keys = [r for r in results if r["status"] == "error"] + + if working_keys: + print("✅ WORKING KEYS:") + for r in working_keys: + print(f" • {r['key_name']}: {r.get('key_preview', 'N/A')}") + print() + + if exhausted_keys: + print("❌ QUOTA EXHAUSTED KEYS:") + for r in exhausted_keys: + print(f" • {r['key_name']}: {r.get('key_preview', 'N/A')}") + print(f" → {r.get('error', 'Quota limit reached')}") + print() + + if not_set_keys: + print("⚠️ NOT CONFIGURED:") + for r in not_set_keys: + print(f" • {r['key_name']}") + print() + + if error_keys: + print("❌ ERROR KEYS:") + for r in error_keys: + print(f" • {r['key_name']}: {r.get('error', 'Unknown error')}") + print() + + print("="*70) + print("💡 Recommendations:") + print("="*70) + + if exhausted_keys: + print("1. Replace exhausted keys with new API keys from Google AI Studio") + print("2. Wait for quota reset (usually resets every minute/hour)") + print("3. Enable billing for higher quotas") + + if len(working_keys) < 3: + print(f"4. Configure {3 - len(working_keys)} more working keys for full per-agent setup") + + print() + print("🔗 Get new API keys: https://aistudio.google.com/app/apikey") + print("📊 Check usage: https://ai.dev/usage?tab=rate-limit") + print() + +if __name__ == "__main__": + main() + From b13f24d9ab38484b1322356fc30b2a17877ceacb Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:01:41 -0800 Subject: [PATCH 15/17] Add files via upload --- frontend/Gemini.png | Bin 0 -> 58444 bytes frontend/ODSC-AI.jpeg | Bin 0 -> 15739 bytes frontend/static/Gemini.png | Bin 0 -> 58444 bytes frontend/static/ODSC-AI.jpeg | Bin 0 -> 15739 bytes frontend/static/css/style.css | 551 +++++++++++++++++++++++ frontend/static/js/script.js | 312 +++++++++++++ frontend/templates/index.html | 144 ++++++ src/__init__.py | 3 + src/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 155 bytes src/__pycache__/executor.cpython-311.pyc | Bin 0 -> 4428 bytes src/__pycache__/memory.cpython-311.pyc | Bin 0 -> 2235 bytes src/__pycache__/planner.cpython-311.pyc | Bin 0 -> 3795 bytes src/executor.py | 76 ++++ src/memory.py | 28 ++ src/planner.py | 76 ++++ 15 files changed, 1190 insertions(+) create mode 100644 frontend/Gemini.png create mode 100644 frontend/ODSC-AI.jpeg create mode 100644 frontend/static/Gemini.png create mode 100644 frontend/static/ODSC-AI.jpeg create mode 100644 frontend/static/css/style.css create mode 100644 frontend/static/js/script.js create mode 100644 frontend/templates/index.html create mode 100644 src/__init__.py create mode 100644 src/__pycache__/__init__.cpython-311.pyc create mode 100644 src/__pycache__/executor.cpython-311.pyc create mode 100644 src/__pycache__/memory.cpython-311.pyc create mode 100644 src/__pycache__/planner.cpython-311.pyc create mode 100644 src/executor.py create mode 100644 src/memory.py create mode 100644 src/planner.py diff --git a/frontend/Gemini.png b/frontend/Gemini.png new file mode 100644 index 0000000000000000000000000000000000000000..dbbf48d60f6e7bda402833956de984048668ba0a GIT binary patch literal 58444 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 J%X%M6{}*Y^L$m+@ literal 0 HcmV?d00001 diff --git a/frontend/static/Gemini.png b/frontend/static/Gemini.png new file mode 100644 index 0000000000000000000000000000000000000000..dbbf48d60f6e7bda402833956de984048668ba0a GIT binary patch literal 58444 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 J%X%M6{}*Y^L$m+@ literal 0 HcmV?d00001 diff --git a/frontend/static/css/style.css b/frontend/static/css/style.css new file mode 100644 index 000000000..66d1727c6 --- /dev/null +++ b/frontend/static/css/style.css @@ -0,0 +1,551 @@ +/* frontend/static/css/style.css */ +body { + font-family: 'Google Sans', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: #333; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + margin: 0; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 30px auto; + background: #fff; + padding: 30px; + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); +} + +/* Header Section with Logos */ +.header-section { + text-align: center; + margin-bottom: 40px; + padding-bottom: 30px; + border-bottom: 2px solid #e8eaed; +} + +.logo-container { + display: flex; + justify-content: center; + align-items: center; + gap: 30px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.logo { + height: 60px; + width: auto; + object-fit: contain; + transition: transform 0.3s ease; +} + +.logo:hover { + transform: scale(1.05); +} + +.gemini-logo { + max-height: 60px; +} + +.odsc-logo { + max-height: 60px; + border-radius: 8px; +} + +h1 { + color: #202124; + text-align: center; + margin-bottom: 15px; + font-size: 2.5rem; + font-weight: 400; +} + +h2 { + color: #202124; + text-align: center; + margin-bottom: 20px; + font-size: 1.75rem; + font-weight: 400; +} + +h3 { + color: #202124; + margin-bottom: 15px; + font-weight: 500; +} + +p { + margin-bottom: 10px; +} + +.input-section { + background: linear-gradient(135deg, #e8f0fe 0%, #f1f3f4 100%); + padding: 25px; + border-radius: 12px; + margin-bottom: 30px; + border: 1px solid #dadce0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +textarea { + width: calc(100% - 20px); + padding: 10px; + margin-bottom: 15px; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 1rem; + box-sizing: border-box; +} + +input[type="file"] { + margin-bottom: 15px; + display: block; + width: 100%; +} + +button { + display: block; + width: 100%; + padding: 12px 20px; + background-color: #4285F4; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1.1rem; + font-weight: 500; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(66, 133, 244, 0.2); +} + +button:hover { + background-color: #3367d6; + box-shadow: 0 4px 8px rgba(66, 133, 244, 0.3); + transform: translateY(-1px); +} + +#loading { + text-align: center; + padding: 10px; + font-style: italic; + color: #555; +} + +.results-section { + margin-top: 30px; + border-top: 1px solid #eee; + padding-top: 20px; +} + +.result-card img { + max-width: 100%; + height: auto; + border-radius: 5px; + margin-top: 10px; + border: 1px solid #ddd; +} + + +.result-card { + background-color: #f9f9f9; + border: 1px solid #e1e1e1; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.result-card h3 { + color: #34495e; + margin-top: 0; + margin-bottom: 15px; + text-align: left; + border-bottom: 1px solid #eee; + padding-bottom: 10px; +} + +.result-card p strong { + color: #555; +} + +#entities_list { + list-style-type: disc; + margin-left: 20px; + padding: 0; +} + +#entities_list li { + margin-bottom: 5px; + color: #666; +} + +pre { + background-color: #eee; + padding: 10px; + border-radius: 5px; + overflow-x: auto; + white-space: pre-wrap; + word-wrap: break-word; +} + +.error { + color: #d9534f; + background-color: #f2dede; + border: 1px solid #ebccd1; + padding: 10px; + border-radius: 5px; + margin-bottom: 20px; +} + +.hidden { + display: none; +} + +/* Orchestration Section Styles */ +.orchestration-section { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; + padding: 30px; + border-radius: 10px; + margin-bottom: 30px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.orchestration-section h2 { + color: white; + margin-bottom: 15px; + text-align: center; +} + +.orchestration-description { + color: rgba(255, 255, 255, 0.95); + text-align: center; + line-height: 1.8; + margin-bottom: 25px; + font-size: 1.05rem; +} + +.orchestration-description strong { + color: white; + font-weight: 600; +} + +.orchestration-flow { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 10px; + margin-top: 20px; + padding: 20px; + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(10px); + border-radius: 8px; +} + +.flow-step { + background: rgba(255, 255, 255, 0.25); + padding: 12px 20px; + border-radius: 8px; + font-weight: 600; + font-size: 0.95rem; + border: 2px solid rgba(255, 255, 255, 0.3); + white-space: nowrap; +} + +.flow-arrow { + font-size: 1.5rem; + font-weight: bold; + color: white; +} + +/* Agent Overview Styles - Google Colors */ +.agents-overview { + background: #ffffff; + padding: 40px 30px; + border-radius: 12px; + margin-bottom: 30px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + border: 1px solid #e8eaed; +} + +.agents-overview h2 { + color: #202124; + margin-bottom: 30px; + font-size: 2rem; + font-weight: 500; +} + +.agent-cards { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + margin-top: 20px; +} + +.agent-card { + background: #ffffff; + padding: 25px 20px; + border-radius: 12px; + border: 2px solid #e8eaed; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + text-align: center; +} + +.agent-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); +} + +.agent-icon { + font-size: 3rem; + margin-bottom: 15px; + display: block; +} + +.agent-card h3 { + color: #202124; + margin-top: 0; + margin-bottom: 15px; + text-align: center; + font-size: 1.2rem; + font-weight: 500; + border-bottom: none; + padding-bottom: 0; +} + +.agent-card p { + color: #5f6368; + margin: 0; + font-size: 0.95rem; + line-height: 1.6; + text-align: left; +} + +/* Google Brand Colors for Agent Cards */ +.agent-blue { + border-top: 4px solid #4285F4; +} + +.agent-blue:hover { + border-color: #4285F4; + box-shadow: 0 8px 16px rgba(66, 133, 244, 0.2); +} + +.agent-red { + border-top: 4px solid #EA4335; +} + +.agent-red:hover { + border-color: #EA4335; + box-shadow: 0 8px 16px rgba(234, 67, 53, 0.2); +} + +.agent-yellow { + border-top: 4px solid #FBBC04; +} + +.agent-yellow:hover { + border-color: #FBBC04; + box-shadow: 0 8px 16px rgba(251, 188, 4, 0.2); +} + +.agent-green { + border-top: 4px solid #34A853; +} + +.agent-green:hover { + border-color: #34A853; + box-shadow: 0 8px 16px rgba(52, 168, 83, 0.2); +} + +.subtitle { + text-align: center; + color: #5f6368; + font-size: 1.1rem; + margin-bottom: 30px; + font-weight: 400; +} + +/* Demo Section Styles */ +.demo-section { + background-color: #f8f9fa; + padding: 25px; + border-radius: 10px; + margin-bottom: 30px; + border: 2px solid #e9ecef; +} + +.demo-section h2 { + color: #2c3e50; + margin-bottom: 10px; +} + +.demo-description { + color: #666; + margin-bottom: 20px; + text-align: center; +} + +.demo-buttons { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; +} + +.demo-btn { + background: #4285F4; + color: white; + border: none; + padding: 15px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 0.95rem; + font-weight: 500; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(66, 133, 244, 0.2); +} + +.demo-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(66, 133, 244, 0.3); + background: #3367d6; +} + +.demo-btn:active { + transform: translateY(0); +} + +.file-label { + display: block; + margin-top: 5px; + margin-bottom: 15px; + color: #666; + font-size: 0.9rem; +} + +/* Loading Spinner - Google Blue */ +.spinner { + border: 4px solid #e8f0fe; + border-top: 4px solid #4285F4; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 0 auto 10px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +#loading { + text-align: center; + padding: 20px; + background-color: #e8f0fe; + border-radius: 8px; + margin-top: 15px; +} + +#loading p { + margin-top: 10px; + color: #555; + font-style: italic; +} + +/* Enhanced Result Cards - Google Colors */ +.result-card { + background: linear-gradient(to bottom, #ffffff 0%, #f8f9fa 100%); + border-left: 4px solid #4285F4; +} + +#knowledge_update_card { + border-left-color: #EA4335; + background: linear-gradient(to bottom, #fef7f7 0%, #f8f9fa 100%); +} + +.knowledge-description { + margin-top: 10px; + padding: 10px; + background-color: #f0f0f0; + border-radius: 5px; + font-style: italic; + color: #666; + font-size: 0.9rem; +} + +.result-card h3 { + color: #202124; + font-weight: 500; +} + +/* Info Box Styles */ +.info-box { + background-color: #e7f3ff; + border: 1px solid #b3d9ff; + border-radius: 8px; + padding: 15px; + margin-top: 20px; + font-size: 0.9rem; +} + +.info-box h4 { + color: #0066cc; + margin-top: 0; + margin-bottom: 10px; + text-align: left; +} + +.info-box ul { + margin: 10px 0; + padding-left: 20px; +} + +.info-box li { + margin-bottom: 5px; + color: #555; +} + +.info-box small { + color: #666; + font-style: italic; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .agent-cards { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .agent-cards { + grid-template-columns: 1fr; + } + + .demo-buttons { + grid-template-columns: 1fr; + } + + .logo-container { + flex-direction: column; + gap: 15px; + } + + .logo { + height: 50px; + } + + h1 { + font-size: 2rem; + } + + h2 { + font-size: 1.5rem; + } +} + diff --git a/frontend/static/js/script.js b/frontend/static/js/script.js new file mode 100644 index 000000000..6c8756d8e --- /dev/null +++ b/frontend/static/js/script.js @@ -0,0 +1,312 @@ +// frontend/static/js/script.js +document.addEventListener('DOMContentLoaded', () => { + const messageForm = document.getElementById('messageForm'); + const loadingIndicator = document.getElementById('loading'); + const errorMessage = document.getElementById('error_message'); + const textContent = document.getElementById('text_content'); + + // Demo examples data + const demoExamples = { + frustrated: { + text: "I am really frustrated with the constant delays on this feature. We've been waiting for 3 weeks now and the deadline is approaching fast. This is becoming a serious issue for our team and we need to accelerate immediately!", + description: "Shows how the system detects high frustration and suggests clarification interventions." + }, + timeline: { + text: "The project timeline is slipping again. We were supposed to deliver Phase 2 by Friday, but now it looks like it will be delayed by at least another week. This is the third time we've had to push back deadlines. We need to have a serious discussion about resource allocation and priorities.", + description: "Demonstrates timeline friction detection and action item proposals." + }, + positive: { + text: "Great work on the latest sprint! The team really pulled together and we delivered everything on time. The new features are working perfectly and the client is very happy with the progress. Let's keep this momentum going!", + description: "Shows how the system recognizes positive communication and confirms no friction." + }, + conflict: { + text: "I completely disagree with the approach we're taking. The current design doesn't align with our original requirements and I think we're going in the wrong direction. We need to stop and reconsider before we waste more time and resources on this.", + description: "Highlights conflict detection and mediation intervention suggestions." + }, + multimodal: { + text: "This graph shows a significant drop in user engagement over the past month. We went from 85% active users to just 62%. The data suggests there might be an issue with the latest update. Can someone analyze this chart and help us understand what's happening?", + description: "Tests multimodal analysis capabilities with data visualization context." + } + }; + + // Setup demo buttons + document.querySelectorAll('.demo-btn').forEach(btn => { + btn.addEventListener('click', () => { + const exampleKey = btn.getAttribute('data-example'); + const example = demoExamples[exampleKey]; + if (example) { + textContent.value = example.text; + // Scroll to form + messageForm.scrollIntoView({ behavior: 'smooth', block: 'start' }); + // Highlight the textarea briefly + textContent.focus(); + textContent.style.border = '2px solid #667eea'; + setTimeout(() => { + textContent.style.border = '1px solid #ccc'; + }, 2000); + } + }); + }); + + // Result cards + const originalMessageCard = document.getElementById('original_message_card'); + const communicationAnalysisCard = document.getElementById('communication_analysis_card'); + const knowledgeUpdateCard = document.getElementById('knowledge_update_card'); + const frictionDetectionCard = document.getElementById('friction_detection_card'); + const interventionSuggestionCard = document.getElementById('intervention_suggestion_card'); + + messageForm.addEventListener('submit', async (event) => { + event.preventDefault(); // Prevent default form submission + + // Hide previous results and errors + hideAllCards(); + errorMessage.classList.add('hidden'); + errorMessage.textContent = ''; + loadingIndicator.classList.remove('hidden'); + + const formData = new FormData(messageForm); + + // Display submitted message immediately + document.getElementById('original_text').textContent = formData.get('text_content'); + const originalImage = document.getElementById('original_image'); + const imageFile = formData.get('image_file'); + + if (imageFile && imageFile.size > 0) { + const reader = new FileReader(); + reader.onload = (e) => { + originalImage.src = e.target.result; + originalImage.style.display = 'block'; + }; + reader.readAsDataURL(imageFile); + document.getElementById('original_image_container').classList.remove('hidden'); + } else { + originalImage.src = ''; + originalImage.style.display = 'none'; + document.getElementById('original_image_container').classList.add('hidden'); + } + originalMessageCard.classList.remove('hidden'); // Ensure original message card is visible + + try { + // Update loading message + const loadingText = loadingIndicator.querySelector('p'); + if (loadingText) { + loadingText.textContent = '🤖 Processing through AI agents... Analyzing with Gemini AI...'; + } + + const response = await fetch('/api/process_message', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + loadingIndicator.classList.add('hidden'); + console.log("API Response:", result); + + if (result.error) { + errorMessage.textContent = `Error: ${result.error}`; + errorMessage.classList.remove('hidden'); + return; + } + + // Warnings removed - don't display API quota/fallback information to users + // (Warnings are still logged server-side for debugging) + + displayResults(result); + + // Smooth scroll to results + setTimeout(() => { + document.querySelector('.results-section').scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + }, 100); + + } catch (error) { + loadingIndicator.classList.add('hidden'); + errorMessage.textContent = `An unexpected error occurred: ${error.message}`; + errorMessage.classList.remove('hidden'); + console.error("Fetch error:", error); + } + }); + + function hideAllCards() { + // originalMessageCard.classList.add('hidden'); // Keep original message card visible + communicationAnalysisCard.classList.add('hidden'); + knowledgeUpdateCard.classList.add('hidden'); + frictionDetectionCard.classList.add('hidden'); + interventionSuggestionCard.classList.add('hidden'); + } + + // checkForAPIErrors function removed - API quota/fallback warnings are not shown to users + + function displayResults(result) { + console.log("Full API result:", result); + + // Parse string responses if needed + let commAnalysis = result.communication_analysis; + let frictionDetection = result.friction_detection; + let interventionSuggestion = result.intervention_suggestion; + + // Try to parse if they're strings (Python dict format) + if (typeof commAnalysis === 'string') { + try { + // Try to parse as JSON first + commAnalysis = JSON.parse(commAnalysis); + } catch (e1) { + try { + // If that fails, try replacing single quotes (Python dict format) + commAnalysis = JSON.parse(commAnalysis.replace(/'/g, '"').replace(/None/g, 'null').replace(/True/g, 'true').replace(/False/g, 'false')); + } catch (e2) { + console.warn("Could not parse communication_analysis:", e2, "Raw:", commAnalysis); + } + } + } + if (typeof frictionDetection === 'string') { + try { + frictionDetection = JSON.parse(frictionDetection); + } catch (e1) { + try { + frictionDetection = JSON.parse(frictionDetection.replace(/'/g, '"').replace(/None/g, 'null').replace(/True/g, 'true').replace(/False/g, 'false')); + } catch (e2) { + console.warn("Could not parse friction_detection:", e2, "Raw:", frictionDetection); + } + } + } + if (typeof interventionSuggestion === 'string') { + try { + interventionSuggestion = JSON.parse(interventionSuggestion); + } catch (e1) { + try { + interventionSuggestion = JSON.parse(interventionSuggestion.replace(/'/g, '"').replace(/None/g, 'null').replace(/True/g, 'true').replace(/False/g, 'false')); + } catch (e2) { + console.warn("Could not parse intervention_suggestion:", e2, "Raw:", interventionSuggestion); + } + } + } + + console.log("Parsed commAnalysis:", commAnalysis); + console.log("Parsed frictionDetection:", frictionDetection); + console.log("Parsed interventionSuggestion:", interventionSuggestion); + + // Display Communication Agent Analysis + if (commAnalysis) { + communicationAnalysisCard.classList.remove('hidden'); + // commAnalysis structure: {analysis: {...}, friction: {...}} + const analysis = commAnalysis.analysis || commAnalysis; + console.log("Extracted analysis:", analysis); + + // Handle nested nlp_analysis structure + const nlpAnalysis = analysis?.nlp_analysis || analysis; + console.log("NLP Analysis:", nlpAnalysis); + + if (nlpAnalysis && nlpAnalysis.sentiment) { + const sentiment = nlpAnalysis.sentiment; + const score = (typeof sentiment === 'object' && sentiment.score !== undefined) ? sentiment.score : + (typeof sentiment === 'number' ? sentiment : 0.0); + const magnitude = (typeof sentiment === 'object' && sentiment.magnitude !== undefined) ? sentiment.magnitude : + (typeof sentiment === 'object' ? Math.abs(score) : 'N/A'); + + document.getElementById('sentiment_score').textContent = typeof score === 'number' ? score.toFixed(2) : (score || 'N/A'); + document.getElementById('sentiment_score').style.color = score < -0.2 ? '#d9534f' : score > 0.2 ? '#5cb85c' : '#f0ad4e'; + document.getElementById('sentiment_magnitude').textContent = typeof magnitude === 'number' ? magnitude.toFixed(2) : (magnitude || 'N/A'); + } else { + document.getElementById('sentiment_score').textContent = 'N/A'; + document.getElementById('sentiment_magnitude').textContent = 'N/A'; + } + + const entitiesList = document.getElementById('entities_list'); + entitiesList.innerHTML = ''; + const entities = nlpAnalysis?.entities || analysis?.entities || []; + if (Array.isArray(entities) && entities.length > 0) { + entities.forEach(entity => { + const listItem = document.createElement('li'); + const entityName = entity.name || 'Unknown'; + const entityType = entity.type_ || entity.type || 'Unknown'; + const salience = entity.salience !== undefined ? entity.salience.toFixed(2) : 'N/A'; + listItem.innerHTML = `${entityName} (${entityType}) - Salience: ${salience}`; + entitiesList.appendChild(listItem); + }); + } else { + entitiesList.innerHTML = '

  • No entities detected
  • '; + } + + const geminiResponse = document.getElementById('gemini_response'); + const geminiText = analysis?.gemini_response_text || analysis?.gemini_response || "No response available"; + geminiResponse.textContent = geminiText; + + // Style response box (no API source indicators shown to users) + geminiResponse.style.backgroundColor = '#eee'; + geminiResponse.style.border = 'none'; + } + + // Display Knowledge Base Update Status + if (result.knowledge_update_status) { + knowledgeUpdateCard.classList.remove('hidden'); + document.getElementById('knowledge_status').textContent = result.knowledge_update_status; + } + + // Display Friction Detection Results + if (frictionDetection) { + frictionDetectionCard.classList.remove('hidden'); + // Handle both boolean and string values + const frictionDetected = frictionDetection.friction_detected === true || + frictionDetection.friction_detected === 'True' || + (typeof frictionDetection.friction_detected === 'string' && frictionDetection.friction_detected.toLowerCase() === 'true'); + + const frictionDetectedEl = document.getElementById('friction_detected'); + frictionDetectedEl.textContent = frictionDetected ? '⚠️ Yes' : '✅ No'; + frictionDetectedEl.style.color = frictionDetected ? '#d9534f' : '#5cb85c'; + frictionDetectedEl.style.fontWeight = 'bold'; + + let frictionReason = frictionDetection.reason || 'No friction detected'; + // Truncate very long reasons for display + if (frictionReason.length > 500) { + frictionReason = frictionReason.substring(0, 500) + '... (truncated)'; + } + document.getElementById('friction_reason').textContent = frictionReason; + + // No API source indicators shown to users + + const severity = frictionDetection.severity; + const severityEl = document.getElementById('friction_severity'); + if (severity !== undefined && severity !== null && severity !== 'N/A') { + const severityNum = typeof severity === 'number' ? severity : parseFloat(severity); + if (!isNaN(severityNum)) { + severityEl.textContent = severityNum.toFixed(2) + ' / 1.0'; + severityEl.style.color = severityNum > 0.7 ? '#d9534f' : severityNum > 0.4 ? '#f0ad4e' : '#5cb85c'; + } else { + severityEl.textContent = 'N/A'; + } + } else { + severityEl.textContent = 'N/A'; + } + } + + // Display Intervention Suggestion + if (interventionSuggestion) { + interventionSuggestionCard.classList.remove('hidden'); + // Handle both boolean and string values + const interventionSuggested = interventionSuggestion.intervention_suggested === true || + interventionSuggestion.intervention_suggested === 'True' || + (typeof interventionSuggestion.intervention_suggested === 'string' && interventionSuggestion.intervention_suggested.toLowerCase() === 'true'); + + const interventionSuggestedEl = document.getElementById('intervention_suggested'); + interventionSuggestedEl.textContent = interventionSuggested ? '💡 Yes' : '✅ No intervention needed'; + interventionSuggestedEl.style.color = interventionSuggested ? '#667eea' : '#5cb85c'; + interventionSuggestedEl.style.fontWeight = 'bold'; + + let suggestionText = interventionSuggestion.suggestion || 'No specific intervention needed at this time.'; + // Truncate very long suggestions for display + if (suggestionText.length > 1000) { + suggestionText = suggestionText.substring(0, 1000) + '... (truncated)'; + } + document.getElementById('intervention_text').textContent = suggestionText; + document.getElementById('intervention_text').style.fontStyle = interventionSuggested ? 'normal' : 'italic'; + } + } +}); + diff --git a/frontend/templates/index.html b/frontend/templates/index.html new file mode 100644 index 000000000..8f102af74 --- /dev/null +++ b/frontend/templates/index.html @@ -0,0 +1,144 @@ + + + + + + CIFR Agent System + + + +
    + +
    +
    + + +
    +

    🤖 CIFR Agent System

    +

    AI-Powered Collaboration Intelligence & Friction Reduction

    +
    + + +
    +

    🎯 CIFR Orchestration System

    +

    + The CIFR (Collaborative Intelligence & Friction Reduction) Agent System is an intelligent orchestration platform + that coordinates multiple specialized AI agents to monitor, analyze, and improve team collaboration. The system processes + collaboration messages through a sophisticated pipeline where each agent plays a critical role in understanding context, + detecting issues, and providing actionable insights. +

    +
    +
    📥 Input
    +
    +
    💬 Communication
    +
    +
    📚 Knowledge
    +
    +
    ⚠️ Friction
    +
    +
    💡 Intervention
    +
    +
    📤 Output
    +
    +
    + + +
    +

    Meet Your AI Agents

    +
    +
    +
    💬
    +

    Communication Agent

    +

    Analyzes messages using Gemini AI to extract sentiment, entities, and context from text and images. Provides deep understanding of communication patterns and emotional tone.

    +
    +
    +
    📚
    +

    Knowledge Agent

    +

    Acts as the central memory hub, storing and retrieving context from all communications. Maintains a knowledge base that enables agents to understand patterns and historical context.

    +
    +
    +
    ⚠️
    +

    Friction Detection Agent

    +

    Identifies misalignments, conflicts, and potential friction points in team communications. Uses AI reasoning to detect subtle patterns and escalation risks.

    +
    +
    +
    💡
    +

    Intervention Agent

    +

    Provides intelligent, actionable suggestions to resolve friction and improve collaboration. Generates context-aware recommendations for clarification, action items, or mediation.

    +
    +
    +
    + + +
    +

    🚀 Try Demo Examples

    +

    Click any example below to see the AI agents in action:

    +
    + + + + + +
    +
    + +
    +

    📝 Custom Message Analysis

    +
    + + + + +
    + +
    + +
    +

    Analysis Results

    + + +
    +

    Submitted Message

    +

    Text:

    + +
    + + + + + + + + +
    +
    + + + diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 000000000..3ea5deb8e --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,3 @@ +# Core hackathon modules package + + diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b86ab11b52e0d4aae794ceeb8df4599e5806d54 GIT binary patch literal 155 zcmZ3^%ge<81Tro5naV)=F^B^Lj8MjBHXvg!U87tB($eq0vb*)IrYu(Bhj)_ zBLzB~eLM5!y*KYOv+wcqwl*Jvw)5^0<(C13{z)f|=Bf`~oP)t4!U%I&l;cud&XIED zoGE9{m2z&kWH&9(d4=;_5!_Gd+`~( z77;-y4x^L`Op9md|TgQPT#|6U;(I%_Pz) zP8j)ob}*eGs*#Y>*{nXm$ltqFAhMw+6!j*&2~IEy9jFtNgcK5(6o`t6Dk~t5+$v-h zSuqmhc`ac~ki9PDP0n$%IdL(b=NQ>r4TssAM=)$ShGAT)H2i2>WK zue2+wL9~xRxpiD5{D9KvvC-Nn2vXny zVDmL?lf3QHNp}2|gdrf*sUb9SiNv=9gJU|;^g&WctH$66(I<_3Ven+b%Y(Wm50ZMk z2MW`+AW4d<7?Ly-X&i%r28!yWzYX9Hs)kYD!8w;DBr1E3()X@P&jI?5SKu1r3iwS^qfbqJzvd*nQX>Xu>i8Zo9cRc>5uN=<#o z;;;uiwTO(S*Acqe97A-Rt=D)KqHwbrlTtGtRAi%J4)|Qy@xal_%b^UKb;F8t{aTHn z1;^EcvwV>+ZPczI3O*$dL7OK{jb=SX4|Y#74YaF8#|XN5c~$_On(;1eJZ()mMd2km z-q)~$FA7C3_H4GJ=q}NNv#Aw%EUepxdEoXwaJKHuq50JOZLq>ux8hS~#jj=s>@Nyi zcp-q>T7CH&^jpWQ4{Q(`N|ZL-h6WCyqOW!L0;ix^Kk%^xK7ly-5W)6r6NI`T+s9%a|?@ z`q9>>vW6Y1C19zMknOIQ@-^KAL`t@=E>TjjEnrB%Qclmv`2ib!KgVvII6#HdfwZEBd5TC=d)K04ZpF6y>R-W`a{H zEf~~9%~44wTi1305fclD7-OipK)2n7mX=8}q|vV(ThrPpfis)W*v3Qg;i)1r$;-OZ;{ia-yb?j|dR6EXgVZyQA*yoT8_*N=D5=ox{on zyHlUaKvp#*Ih)p$@p`#r^R$%Gjbu>kpt{l1pmkF~^MLJVhoxqQHbl|X6Iz@C8iHEz z{dE-le69a_4TVbNAfwwNR(IZQp8WpBdU$iSMo=XC%$l;KGSY zyw{2!d=?*DjgOfZu9oB1toXHslNGW1{^`ZD_s*71Jw0K5aM^q-1-&d@vBWE;c%>rl zSRB1K`eKu?M*7+IZniyzuzipG z^u%9J|K;?T0rSH3^5L{~IBoXG&=*cuVsBcp{m){8pA|DyM<-_87b=F#5e8|C;RD}D%c?%i$mzVod2+-mQ+O5fhkPknak(e%@v za^JgF-@9LSf3@@PJE?!K-zdK?Tkp$eH-`S)>FA4og8(d?sq(0ETRmE%D<1PQF%OJG zFN+yV%$Q=P)#{*`8aI10(97b4B~F;EKaVi;x>2O_zJD=vFH{OHN1uwz!E*SB6+U8y zk5s~4i@|%rN3K5$e-f6($H6axD_(PqnDKGw<#5IdXUuR025a5D%bu0KzwLWA{Nd{G zhvnf5*6;;0bdh%9q85d7uSEbP1GMy3(>Wr+7gCaPc|4V+K0IbClRX3gRsP_j|4;={za9%p{q3yA%pN`&=2=l zz5VV>aA+u83r4DLHsw)A*MhNhtkkh|^8PQZcG!a91=PNMsc%Uy9b3BfV8Dtb>+C+{ z3(m<4Lvs`JZ&dy35^Hid!v?VMo9`fDO8*CcLnu}N literal 0 HcmV?d00001 diff --git a/src/__pycache__/memory.cpython-311.pyc b/src/__pycache__/memory.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..657044c555b05276c6609b10ae587164add74e99 GIT binary patch literal 2235 zcmah}O>7%Q6rR~XZ|o#)>xANpk_}2hj)Dy|tpr3tZ7j7yZIB2cv{JRKb|$G?@2=U| z(8jnZhagfZhk}ISLk>Bh;;NCzfkO@)df8^m#efURBSBN}=GRhM^%-}#JrceEXO*2v;o^uugDPOSsK)O_5 zexQC-@(ZqG7OlPAVW;j5c7Fw76;Y&Pf^>mkT_nO1(j_8NnMhQjGL6j&>CRpvKd+lr zh_WQ)*7Y=clUnnf2d0@0(absO_?qvT7G+rrccMeUIYN*+ zv^xN>isn!S8J%f@X3#x6eGdl;{R%8FAYWp%ILo*R?de$VQ${_Oqb1Yvb9u_H`)(G0)0Hj%7;=fod29r{Eh#d7vwjY+K7y#QFdhQFAQ2EUhG#;kV133F6`L zhQCmvL9Aph6kU@9ibwr<&#}1s?Z&Gv4Ce!?6D zxQ5^|=!mc;-V!V5TIbaDcBOZ3MHTTboOQo*9tI6$QJfC2o)H5%49H8to}h=9GV@Kl zl#xAdHc;k$%W-c6Jq6}MHf_`QI5B}@mP*ti8OiI1p?8c!XhMeMcngesqCxYLe6GJ4T=QJu*(38=+D8S=J8h_r-#>ucG9Db^ys7X_`~%0PWo&keReCglb&eBq@(c` z0$7$?an#edl6a(^c&MJJp4m}{8|rXf9d0HEmV3RUU~$^&W|0RHX5uptzQe2NHeMX) z&INGTVdo%MGiVvlVuF{{?lt*m@&RlL(+6sqhtKKQ9Rv6gj^x#R<;X2ufy=&s32zJ6 zLq~t6m7`z?d&gnRJ;+ZWWZw!f4Y_69e@$Bq>3kZWD8l<X4hD$6kjjh@NZdl0 z_KdKfZpJ;=zK9YW-#@wh(%~NQTg}cyr#BOW^~A}?spC~?EwPgtX{1K#y(7)GPvBW~ zeC>Vcc9K&K(5h3RFHg9Q06T|g_cfS=dxow-AGHcjzzhFv*Bd&6JYIHQFF-pO&PWu@ zUbQMkc+{C+KGq#bq~ob$g8=X{BvUJ2SI<@7sh+N$ZX}25>QLxpcmSmFb$&!Ye`*}l zriKy34a0WHe38q&hVkXRS=^h683u7J!|;y4oz9)soKqrY~$3sq3(YnikQbaor&!|RPZ7foVkdzkiC$ukhqW)4OZ_n^;DgR tk}>JM4HNwOGaH~KVvL*UWc}dRL}T?AeJy1WLrBlV-(2_4ArqlH{{ixq_eB5z literal 0 HcmV?d00001 diff --git a/src/__pycache__/planner.cpython-311.pyc b/src/__pycache__/planner.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15bc4c828ed4b47948ec9247fe49c27228e316c5 GIT binary patch literal 3795 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 literal 0 HcmV?d00001 diff --git a/src/executor.py b/src/executor.py new file mode 100644 index 000000000..a5c9fa2db --- /dev/null +++ b/src/executor.py @@ -0,0 +1,76 @@ +import logging +from typing import Any, Dict, List, Optional +from src.memory import MemoryStore +from src import planner + +logger = logging.getLogger(__name__) + + +class Executor: + """ + Coordinates planning and tool/agent calls. + Expects injected agents to keep dependencies explicit for the hackathon template. + """ + + def __init__( + self, + communication_agent: Any, + friction_detection_agent: Any, + intervention_agent: Any, + knowledge_agent: Any, + memory_store: Optional[MemoryStore] = None, + ): + self.communication_agent = communication_agent + self.friction_detection_agent = friction_detection_agent + self.intervention_agent = intervention_agent + self.knowledge_agent = knowledge_agent + self.memory = memory_store or MemoryStore() + + def execute_plan( + self, + goal: str, + messages: List[Dict[str, Any]], + context: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + plan_result = planner.plan(goal, context) + self.memory.log("plan_created", {"goal": goal, "plan": plan_result}) + + results: List[Dict[str, Any]] = [] + + for step in plan_result["steps"]: + action = step.get("action") + + if action == "analyze_messages": + for message in messages: + analysis = self.communication_agent.process_collaboration_message(message) + self.memory.log("analysis", {"message": message, "analysis": analysis}) + results.append({"step": step["id"], "type": "analysis", "result": analysis}) + + elif action == "detect_friction": + for message in messages: + context_key = f"communication_analysis_{message.get('message_id', 'demo_msg')}" + stored = self.knowledge_agent.retrieve_context(context_key) or {"message": message} + friction = self.friction_detection_agent.detect_misalignment(stored) + self.memory.log("friction_detection", {"message": message, "friction": friction}) + results.append({"step": step["id"], "type": "friction", "result": friction}) + + elif action == "generate_interventions": + for message in messages: + context_key = f"communication_analysis_{message.get('message_id', 'demo_msg')}" + stored = self.knowledge_agent.retrieve_context(context_key) or {"message": message} + friction = stored.get("friction", {}) + intervention = self.intervention_agent.suggest_clarification( + {"message": stored.get("message", {}), "reason": friction.get("reason", "")} + ) + self.memory.log("intervention", {"message": message, "intervention": intervention}) + results.append({"step": step["id"], "type": "intervention", "result": intervention}) + + else: + # Unknown action; log and continue + logger.warning("Skipped unknown action: %s", action) + self.memory.log("skipped_step", {"step": step}) + results.append({"step": step.get("id"), "type": "skipped", "reason": "unknown action"}) + + return {"plan": plan_result, "results": results, "trace": self.memory.latest()} + + diff --git a/src/memory.py b/src/memory.py new file mode 100644 index 000000000..3dde7bf22 --- /dev/null +++ b/src/memory.py @@ -0,0 +1,28 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + + +class MemoryStore: + """Lightweight in-memory log for executions and agent traces.""" + + def __init__(self): + self.events: List[Dict[str, Any]] = [] + + def log(self, event_type: str, payload: Dict[str, Any]) -> Dict[str, Any]: + entry = { + "event": event_type, + "payload": payload, + "timestamp": datetime.utcnow().isoformat() + "Z", + } + self.events.append(entry) + return entry + + def get_events(self, event_type: Optional[str] = None) -> List[Dict[str, Any]]: + if event_type is None: + return list(self.events) + return [e for e in self.events if e["event"] == event_type] + + def latest(self, n: int = 20) -> List[Dict[str, Any]]: + return self.events[-n:] + + diff --git a/src/planner.py b/src/planner.py new file mode 100644 index 000000000..525e5344f --- /dev/null +++ b/src/planner.py @@ -0,0 +1,76 @@ +import json +import os +from typing import Any, Dict, List, Optional + +try: + import google.genai as genai +except ImportError: + genai = None + + +def _make_client() -> Optional[Any]: + """Create a Gemini client if api key and library are available.""" + api_key = os.getenv("GOOGLE_API_KEY") + if not api_key or genai is None: + return None + try: + return genai.Client(api_key=api_key) + except Exception: + return None + + +def _parse_candidate(raw_text: str) -> List[Dict[str, Any]]: + """Parse Gemini JSON output into a list of steps.""" + try: + data = json.loads(raw_text) + if isinstance(data, dict) and "steps" in data and isinstance(data["steps"], list): + return data["steps"] + if isinstance(data, list): + return data + except Exception: + pass + return [] + + +def plan(goal: str, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + Produce a task plan for the goal. + Returns {source, steps, raw_response, error}. + """ + context = context or {} + steps: List[Dict[str, Any]] = [] + raw_response = None + error = None + + client = _make_client() + if goal and client: + prompt = ( + "You are a planner. Create 3-6 JSON steps to satisfy the goal. " + "Each step must have: id, action, input, notes, expected_output. " + f"Goal: {goal}\nContext: {json.dumps(context)[:1500]}" + ) + try: + response = client.models.generate_content( + model=os.getenv("GEMINI_PRO_MODEL_ID", "gemini-2.0-flash"), + contents=[{"parts": [{"text": prompt}]}], + ) + if response.candidates and response.candidates[0].content.parts: + raw_response = response.candidates[0].content.parts[0].text + steps = _parse_candidate(raw_response) + except Exception as exc: # pragma: no cover - network/Gemini issues + error = str(exc) + + if not steps: + # Fallback heuristic plan + steps = [ + {"id": "1", "action": "analyze_messages", "input": "ingest and analyze messages", "notes": "use CommunicationAgent", "expected_output": "message analyses"}, + {"id": "2", "action": "detect_friction", "input": "use analyses", "notes": "call FrictionDetectionAgent", "expected_output": "friction report"}, + {"id": "3", "action": "generate_interventions", "input": "friction report", "notes": "call InterventionAgent", "expected_output": "recommended actions"}, + ] + source = "heuristic" + else: + source = "gemini" + + return {"source": source, "steps": steps, "raw_response": raw_response, "error": error} + + From b72aaf5586f1a23873bf7f7cb662db91ae854ecf Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:56:15 -0800 Subject: [PATCH 16/17] Delete FALLBACK_SERVICE_SETUP.md --- FALLBACK_SERVICE_SETUP.md | 128 -------------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 FALLBACK_SERVICE_SETUP.md diff --git a/FALLBACK_SERVICE_SETUP.md b/FALLBACK_SERVICE_SETUP.md deleted file mode 100644 index f6d994468..000000000 --- a/FALLBACK_SERVICE_SETUP.md +++ /dev/null @@ -1,128 +0,0 @@ -# 🔄 OpenAI-Compatible Fallback Service Setup - -## Overview - -The CIFR Agent System now includes an **optional fallback mechanism** that automatically switches to an OpenAI-compatible service when the free tier Google Gemini API hits quota limits (429 errors). - -**⚠️ Important for Hackathon:** -- **Primary Service:** Free tier Google Gemini API (for hackathon judges) -- **Fallback Service:** OpenAI-compatible service (demo/backup only - NOT available to hackathon judges) -- The system will **always try the free tier first** - fallback is only used when quota is exhausted - -## How It Works - -1. **Primary:** System tries Google Gemini API (free tier) -2. **On 429 Error:** If quota exhausted, automatically tries OpenAI-compatible fallback -3. **If Both Fail:** Falls back to heuristic analysis (for hackathon demo) - -## Environment Variables - -Add these to your `.env` file or set as environment variables: - -```env -# Enable fallback service (set to "1" to enable, "0" to disable) -ENABLE_OPENAI_FALLBACK=1 - -# OpenAI-compatible service base URL -OPENAI_FALLBACK_BASE_URL=https://caas-gocode-prod.caas-prod.prod.onkatana.net/v1 - -# OpenAI-compatible API key -OPENAI_FALLBACK_API_KEY=sk-k5xL1aEj4Vi0YPPJ3pdxMw - -# Model name on the fallback service (must match available models) -OPENAI_FALLBACK_MODEL=gemini-2.0-flash-001 -``` - -## Setup Instructions - -### Option 1: Add to .env File - -Add these lines to your `.env` file: - -```env -ENABLE_OPENAI_FALLBACK=1 -OPENAI_FALLBACK_BASE_URL=https://caas-gocode-prod.caas-prod.prod.onkatana.net/v1 -OPENAI_FALLBACK_API_KEY=sk-k5xL1aEj4Vi0YPPJ3pdxMw -OPENAI_FALLBACK_MODEL=gemini-2.0-flash-001 -``` - -### Option 2: Set as Environment Variables - -When starting the Flask app: - -```bash -ENABLE_OPENAI_FALLBACK=1 \ -OPENAI_FALLBACK_BASE_URL='https://caas-gocode-prod.caas-prod.prod.onkatana.net/v1' \ -OPENAI_FALLBACK_API_KEY='sk-k5xL1aEj4Vi0YPPJ3pdxMw' \ -OPENAI_FALLBACK_MODEL='gemini-2.0-flash-001' \ -python3 -c "from app import app; app.run(debug=True, host='127.0.0.1', port=9000)" -``` - -## Available Models on Fallback Service - -The fallback service supports these Gemini models: -- `gemini-2.0-flash-001` ✅ (Recommended) -- `gemini-2.0-flash-exp` -- `gemini-2.5-flash` -- `gemini-2.5-flash-image` -- `gemini-2.5-pro` -- And more... - -## Behavior - -### When Fallback is Enabled (`ENABLE_OPENAI_FALLBACK=1`): - -1. **Free Tier Works:** Uses Google Gemini API (primary) -2. **Free Tier Exhausted (429):** Automatically switches to OpenAI-compatible service -3. **Both Fail:** Uses heuristic analysis (for hackathon demo) - -### When Fallback is Disabled (`ENABLE_OPENAI_FALLBACK=0` or not set): - -1. **Free Tier Works:** Uses Google Gemini API -2. **Free Tier Exhausted (429):** Uses heuristic analysis (for hackathon demo) - -## For Hackathon Judges - -**Important:** Hackathon judges will **NOT** have access to the fallback service. The system is designed to: - -1. **Primary:** Use free tier Google Gemini API (available to all) -2. **Fallback:** Use heuristic analysis when API quota is exhausted -3. **Optional:** Fallback service is only for your personal demo/backup - -The system will work perfectly for hackathon judges using only the free tier API and heuristic fallback. - -## Testing - -To test if the fallback is working: - -1. Set `ENABLE_OPENAI_FALLBACK=1` -2. Use a Google Gemini API key that's exhausted (429 error) -3. Make an API call - it should automatically use the fallback service -4. Check logs for: "Using OpenAI-compatible fallback service" - -## Troubleshooting - -### Fallback Not Working? - -1. **Check if enabled:** `ENABLE_OPENAI_FALLBACK=1` -2. **Check base URL:** Must be correct OpenAI-compatible endpoint -3. **Check API key:** Must be valid for the service -4. **Check model name:** Must match available models on the service -5. **Check logs:** Look for error messages in console - -### Service Not Available? - -If the fallback service is not accessible: -- System will automatically use heuristic analysis -- Hackathon demo will still work -- No action needed - this is expected behavior - -## Summary - -- ✅ **Primary:** Free tier Google Gemini API (for hackathon) -- ✅ **Fallback:** OpenAI-compatible service (demo/backup only) -- ✅ **Final Fallback:** Heuristic analysis (always works) -- ⚠️ **Hackathon Judges:** Will only use primary + heuristic (no fallback service access) - -The system is designed to work perfectly for hackathon judges while providing you with a backup option for demos! 🚀 - From 1e3474126e951f7fbb552ecdc9597de8115fa027 Mon Sep 17 00:00:00 2001 From: Ekta Pant <156365091+pante008@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:56:37 -0800 Subject: [PATCH 17/17] Delete API_KEY_SETUP.md --- API_KEY_SETUP.md | 124 ----------------------------------------------- 1 file changed, 124 deletions(-) delete mode 100644 API_KEY_SETUP.md diff --git a/API_KEY_SETUP.md b/API_KEY_SETUP.md deleted file mode 100644 index 1376f4331..000000000 --- a/API_KEY_SETUP.md +++ /dev/null @@ -1,124 +0,0 @@ -# 🔑 Per-Agent API Key Setup Guide - -## Overview -The CIFR Agent System supports **per-agent API keys** to distribute API quota across multiple keys, solving resource exhaustion errors. - -## ✅ Current Configuration - -The system is already configured to use per-agent keys! Each agent will use its dedicated key if available, or fall back to the default key. - -### Agent Key Mapping: -- **Communication Agent** → `GOOGLE_API_KEY_CA` -- **Friction Detection Agent** → `GOOGLE_API_KEY_FA` -- **Intervention Agent** → `GOOGLE_API_KEY_IA` -- **Default Fallback** → `GOOGLE_API_KEY` - -## 🚀 How to Set Up Per-Agent Keys - -### Option 1: Environment Variables (Recommended) - -When starting the Flask app, set all keys: - -```bash -GCP_PROJECT_ID='steel-pod-481123-e9' \ -GCP_LOCATION='us-central1' \ -GOOGLE_API_KEY='AIzaSyA-nv8nAyq8LaCcFk4ZjKk3dV-b_rm_5hA' \ -GOOGLE_API_KEY_CA='AIzaSyAdX9HpiIJ-Z7ruwv6etRUd_5cc-WRcEX0' \ -GOOGLE_API_KEY_FA='AIzaSyA-nv8nAyq8LaCcFk4ZjKk3dV-b_rm_5hA' \ -GOOGLE_API_KEY_IA='AIzaSyDmyetZGSBcQ1QCtee_YtwZAM0iT99e1bM' \ -GEMINI_PRO_MODEL_ID='gemini-2.0-flash' \ -GEMINI_PRO_VISION_MODEL_ID='gemini-2.5-flash' \ -FLASK_SKIP_DOTENV=1 \ -python3 -c "from app import app; app.run(debug=True, host='127.0.0.1', port=9000, load_dotenv=False)" -``` - -### Option 2: .env File - -Add to your `.env` file (if readable): - -```env -GCP_PROJECT_ID=steel-pod-481123-e9 -GCP_LOCATION=us-central1 -GOOGLE_API_KEY=AIzaSyA-nv8nAyq8LaCcFk4ZjKk3dV-b_rm_5hA -GOOGLE_API_KEY_CA=AIzaSyAdX9HpiIJ-Z7ruwv6etRUd_5cc-WRcEX0 -GOOGLE_API_KEY_FA=AIzaSyA-nv8nAyq8LaCcFk4ZjKk3dV-b_rm_5hA -GOOGLE_API_KEY_IA=AIzaSyDmyetZGSBcQ1QCtee_YtwZAM0iT99e1bM -GEMINI_PRO_MODEL_ID=gemini-2.0-flash -GEMINI_PRO_VISION_MODEL_ID=gemini-2.5-flash -``` - -## 📊 Benefits of Per-Agent Keys - -1. **Distributed Quota**: Each agent uses its own quota limit -2. **Higher Throughput**: 3x the free tier quota (if using 3 different keys) -3. **Fault Isolation**: If one key hits quota, other agents continue working -4. **Scalability**: Easy to add more keys as needed - -## 🔍 How to Get Multiple API Keys - -1. Go to [Google AI Studio](https://aistudio.google.com/app/apikey) -2. Create multiple API keys (one for each agent) -3. Name them clearly (e.g., "CIFR-CA", "CIFR-FA", "CIFR-IA") -4. Copy each key and set the appropriate environment variable - -## ✅ Verification - -When you start the app, you'll see logging like: - -``` -🤖 CIFR Agent System - Initializing Agents -============================================================ -📋 Project ID: steel-pod-481123-e9 -📍 Location: us-central1 - -🔑 API Key Configuration: - Default Key: ✅ Set - Communication Agent (CA): ✅ Using dedicated key - Friction Detection (FA): ✅ Using dedicated key - Intervention Agent (IA): ✅ Using dedicated key -============================================================ - -[Communication Agent] Using dedicated API key (CA): AIzaSyAdX9HpiIJ-Z7... -[Friction Detection Agent] Using dedicated API key (FA): AIzaSyA-nv8nAyq8... -[Intervention Agent] Using dedicated API key (IA): AIzaSyDmyetZGSBcQ1... -``` - -## 🎯 Best Practices - -1. **Use Different Keys**: Use 3 different API keys for maximum quota distribution -2. **Monitor Usage**: Check [Google AI Studio Usage](https://ai.dev/usage?tab=rate-limit) regularly -3. **Rotate Keys**: If a key hits quota, you can rotate to a new key -4. **Enable Billing**: For production, enable billing for higher quotas - -## 🚨 Troubleshooting - -### If you still see quota errors: - -1. **Check Key Status**: Verify keys are active in Google AI Studio -2. **Wait for Reset**: Free tier quotas reset periodically -3. **Use More Keys**: Add additional keys for more quota -4. **Enable Billing**: Production use requires billing for higher limits - -### Current Status: - -Based on your `.env` file, you have: -- ✅ `GOOGLE_API_KEY_IA` set -- ⚠️ Need to set `GOOGLE_API_KEY_CA` and `GOOGLE_API_KEY_FA` for full per-agent setup - -## 📝 Example: Full Per-Agent Setup - -```bash -# Get 3 different API keys from Google AI Studio -KEY_1="your-first-api-key" -KEY_2="your-second-api-key" -KEY_3="your-third-api-key" - -# Start app with per-agent keys -GOOGLE_API_KEY_CA=$KEY_1 \ -GOOGLE_API_KEY_FA=$KEY_2 \ -GOOGLE_API_KEY_IA=$KEY_3 \ -python3 -c "from app import app; app.run(debug=True, host='127.0.0.1', port=9000)" -``` - -This distributes the API calls across 3 different keys, giving you 3x the free tier quota! 🚀 -