Skip to content

Commit 731f77f

Browse files
authored
Merge pull request #209 from NPC-Worldwide/caug/activity-logging
Activity logging and autocomplete tracking
2 parents ea1fb7e + 5f19a87 commit 731f77f

2 files changed

Lines changed: 210 additions & 14 deletions

File tree

npcpy/memory/command_history.py

Lines changed: 135 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,41 @@ def _initialize_schema(self):
710710
Column('created_at', DateTime, default=func.now())
711711
)
712712

713+
Table('activity_log', metadata,
714+
Column('id', Integer, primary_key=True, autoincrement=True),
715+
Column('timestamp', String(50), nullable=False),
716+
Column('activity_type', String(50), nullable=False),
717+
Column('activity_data', Text),
718+
Column('directory_path', Text),
719+
Column('npc', String(100)),
720+
Column('device_id', String(255)),
721+
Column('session_id', String(100)),
722+
)
723+
724+
Table('autocomplete_suggestions', metadata,
725+
Column('id', Integer, primary_key=True, autoincrement=True),
726+
Column('timestamp', String(50), nullable=False),
727+
Column('suggestion_type', String(50), nullable=False),
728+
Column('input_context', Text),
729+
Column('suggestion', Text, nullable=False),
730+
Column('accepted', Integer, default=0),
731+
Column('npc', String(100)),
732+
Column('model', String(100)),
733+
Column('provider', String(100)),
734+
Column('directory_path', Text),
735+
)
736+
737+
Table('autocomplete_training', metadata,
738+
Column('id', Integer, primary_key=True, autoincrement=True),
739+
Column('created_at', DateTime, default=func.now()),
740+
Column('suggestion_type', String(50), nullable=False),
741+
Column('input_text', Text, nullable=False),
742+
Column('output_text', Text, nullable=False),
743+
Column('accepted', Integer, nullable=False),
744+
Column('npc', String(100)),
745+
Column('model', String(100)),
746+
)
747+
713748
metadata.create_all(self.engine, checkfirst=True)
714749
init_kg_schema(self.engine)
715750

@@ -1015,22 +1050,32 @@ def get_memories_for_scope(
10151050
directory_path: str,
10161051
status: Optional[str] = None
10171052
) -> List[Dict]:
1018-
1019-
query = """
1020-
SELECT id, initial_memory, final_memory,
1021-
status, timestamp, created_at
1022-
FROM memory_lifecycle
1023-
WHERE npc = :npc AND team = :team AND directory_path = :path
1024-
"""
1025-
params = {"npc": npc, "team": team, "path": directory_path}
1026-
1053+
1054+
conditions = []
1055+
params = {}
1056+
1057+
if npc:
1058+
conditions.append("npc = :npc")
1059+
params["npc"] = npc
1060+
if team:
1061+
conditions.append("team = :team")
1062+
params["team"] = team
1063+
if directory_path:
1064+
conditions.append("directory_path = :path")
1065+
params["path"] = directory_path
10271066
if status:
1028-
query += " AND status = :status"
1067+
conditions.append("status = :status")
10291068
params["status"] = status
1030-
1031-
query += " ORDER BY created_at DESC"
1032-
data =self._fetch_all(query, params)
1033-
return data
1069+
1070+
where = "WHERE " + " AND ".join(conditions) if conditions else ""
1071+
query = f"""
1072+
SELECT id, initial_memory, final_memory,
1073+
status, timestamp, created_at, npc, team, directory_path
1074+
FROM memory_lifecycle
1075+
{where}
1076+
ORDER BY created_at DESC
1077+
"""
1078+
return self._fetch_all(query, params)
10341079

10351080
def search_memory(self, query: str, npc: str = None, team: str = None,
10361081
directory_path: str = None, status_filter: str = None, limit: int = 10):
@@ -1524,6 +1569,82 @@ def get_all_commands(self, limit: int = 100) -> List[Dict]:
15241569
"""
15251570
return self._fetch_all(stmt, {"limit": limit})
15261571

1572+
def log_activity(self, activity_type: str, activity_data: str = None,
1573+
directory_path: str = None, npc: str = None,
1574+
device_id: str = None, session_id: str = None):
1575+
import datetime
1576+
ts = datetime.datetime.now().isoformat()
1577+
with self.engine.begin() as conn:
1578+
conn.execute(text("""
1579+
INSERT INTO activity_log (timestamp, activity_type, activity_data, directory_path, npc, device_id, session_id)
1580+
VALUES (:ts, :atype, :adata, :dpath, :npc, :did, :sid)
1581+
"""), {"ts": ts, "atype": activity_type, "adata": activity_data,
1582+
"dpath": directory_path, "npc": npc, "did": device_id, "sid": session_id})
1583+
1584+
def log_autocomplete(self, suggestion_type: str, input_context: str, suggestion: str,
1585+
accepted: bool, npc: str = None, model: str = None,
1586+
provider: str = None, directory_path: str = None):
1587+
import datetime
1588+
ts = datetime.datetime.now().isoformat()
1589+
with self.engine.begin() as conn:
1590+
conn.execute(text("""
1591+
INSERT INTO autocomplete_suggestions (timestamp, suggestion_type, input_context, suggestion, accepted, npc, model, provider, directory_path)
1592+
VALUES (:ts, :stype, :ctx, :sug, :acc, :npc, :model, :prov, :dpath)
1593+
"""), {"ts": ts, "stype": suggestion_type, "ctx": input_context,
1594+
"sug": suggestion, "acc": 1 if accepted else 0,
1595+
"npc": npc, "model": model, "prov": provider, "dpath": directory_path})
1596+
conn.execute(text("""
1597+
INSERT INTO autocomplete_training (suggestion_type, input_text, output_text, accepted, npc, model)
1598+
VALUES (:stype, :inp, :out, :acc, :npc, :model)
1599+
"""), {"stype": suggestion_type, "inp": input_context, "out": suggestion,
1600+
"acc": 1 if accepted else 0, "npc": npc, "model": model})
1601+
1602+
def get_activities(self, activity_type: str = None, limit: int = 100,
1603+
directory_path: str = None, session_id: str = None):
1604+
conditions = []
1605+
params = {"lim": limit}
1606+
if activity_type:
1607+
conditions.append("activity_type = :atype")
1608+
params["atype"] = activity_type
1609+
if directory_path:
1610+
conditions.append("directory_path = :dpath")
1611+
params["dpath"] = directory_path
1612+
if session_id:
1613+
conditions.append("session_id = :sid")
1614+
params["sid"] = session_id
1615+
where = "WHERE " + " AND ".join(conditions) if conditions else ""
1616+
return self._fetch_all(f"SELECT * FROM activity_log {where} ORDER BY timestamp DESC LIMIT :lim", params)
1617+
1618+
def get_autocomplete_stats(self, suggestion_type: str = None, npc: str = None):
1619+
conditions = []
1620+
params = {}
1621+
if suggestion_type:
1622+
conditions.append("suggestion_type = :stype")
1623+
params["stype"] = suggestion_type
1624+
if npc:
1625+
conditions.append("npc = :npc")
1626+
params["npc"] = npc
1627+
where = "WHERE " + " AND ".join(conditions) if conditions else ""
1628+
return self._fetch_all(f"""
1629+
SELECT suggestion_type,
1630+
COUNT(*) as total,
1631+
SUM(accepted) as accepted,
1632+
COUNT(*) - SUM(accepted) as rejected
1633+
FROM autocomplete_suggestions {where}
1634+
GROUP BY suggestion_type
1635+
""", params)
1636+
1637+
def get_training_data(self, suggestion_type: str = None, accepted_only: bool = False, limit: int = 1000):
1638+
conditions = []
1639+
params = {"lim": limit}
1640+
if suggestion_type:
1641+
conditions.append("suggestion_type = :stype")
1642+
params["stype"] = suggestion_type
1643+
if accepted_only:
1644+
conditions.append("accepted = 1")
1645+
where = "WHERE " + " AND ".join(conditions) if conditions else ""
1646+
return self._fetch_all(f"SELECT * FROM autocomplete_training {where} ORDER BY created_at DESC LIMIT :lim", params)
1647+
15271648
def close(self):
15281649
"""Dispose of the SQLAlchemy engine."""
15291650
if self.engine:

npcpy/serve.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6392,6 +6392,81 @@ def get_memories_by_scope():
63926392
traceback.print_exc()
63936393
return jsonify({"error": str(e)}), 500
63946394

6395+
@app.route("/api/activity/log", methods=["POST"])
6396+
def log_activity():
6397+
try:
6398+
data = request.json or {}
6399+
ch = CommandHistory(app.config.get('DB_PATH'))
6400+
ch.log_activity(
6401+
activity_type=data.get("type", "unknown"),
6402+
activity_data=json.dumps(data.get("data")) if data.get("data") else None,
6403+
directory_path=data.get("directoryPath"),
6404+
npc=data.get("npc"),
6405+
device_id=data.get("deviceId"),
6406+
session_id=data.get("sessionId"),
6407+
)
6408+
return jsonify({"success": True})
6409+
except Exception as e:
6410+
return jsonify({"error": str(e)}), 500
6411+
6412+
@app.route("/api/activity/list", methods=["GET"])
6413+
def list_activities():
6414+
try:
6415+
ch = CommandHistory(app.config.get('DB_PATH'))
6416+
activities = ch.get_activities(
6417+
activity_type=request.args.get("type"),
6418+
limit=int(request.args.get("limit", 100)),
6419+
directory_path=request.args.get("directoryPath"),
6420+
session_id=request.args.get("sessionId"),
6421+
)
6422+
return jsonify({"activities": activities})
6423+
except Exception as e:
6424+
return jsonify({"error": str(e)}), 500
6425+
6426+
@app.route("/api/autocomplete/log", methods=["POST"])
6427+
def log_autocomplete():
6428+
try:
6429+
data = request.json or {}
6430+
ch = CommandHistory(app.config.get('DB_PATH'))
6431+
ch.log_autocomplete(
6432+
suggestion_type=data.get("type", "text"),
6433+
input_context=data.get("inputContext", ""),
6434+
suggestion=data.get("suggestion", ""),
6435+
accepted=data.get("accepted", False),
6436+
npc=data.get("npc"),
6437+
model=data.get("model"),
6438+
provider=data.get("provider"),
6439+
directory_path=data.get("directoryPath"),
6440+
)
6441+
return jsonify({"success": True})
6442+
except Exception as e:
6443+
return jsonify({"error": str(e)}), 500
6444+
6445+
@app.route("/api/autocomplete/stats", methods=["GET"])
6446+
def autocomplete_stats():
6447+
try:
6448+
ch = CommandHistory(app.config.get('DB_PATH'))
6449+
stats = ch.get_autocomplete_stats(
6450+
suggestion_type=request.args.get("type"),
6451+
npc=request.args.get("npc"),
6452+
)
6453+
return jsonify({"stats": stats})
6454+
except Exception as e:
6455+
return jsonify({"error": str(e)}), 500
6456+
6457+
@app.route("/api/autocomplete/training", methods=["GET"])
6458+
def autocomplete_training_data():
6459+
try:
6460+
ch = CommandHistory(app.config.get('DB_PATH'))
6461+
data = ch.get_training_data(
6462+
suggestion_type=request.args.get("type"),
6463+
accepted_only=request.args.get("acceptedOnly") == "true",
6464+
limit=int(request.args.get("limit", 1000)),
6465+
)
6466+
return jsonify({"data": data, "count": len(data)})
6467+
except Exception as e:
6468+
return jsonify({"error": str(e)}), 500
6469+
63956470
@app.route("/api/interrupt", methods=["POST"])
63966471
def interrupt_stream():
63976472
data = request.json

0 commit comments

Comments
 (0)