Competition for Customer Service

Sitting on hold with AT&T and what I’ve really noticed is besides being on the phone with them for an hour is how customer service is handled.  They’ve done everything “right” however what’s interesting is to think about how the dialog has gone as it compares to all of my training via Vail/Northstar Ski School.

The key words I keep on hearing are “I’m Sorry”, “I apologize for that”, “It won’t let me do that”, “Please wait”, “So sorry about this”.  What’s interesting to compare and contrast is the mantra Northstar mantra of “Make the Customer Right”.  When I’m working with customers at Northstar my perspective is how can I help you have a better day.  Sure something has gone wrong, but lets figure out how to get past that point and move to someplace that you can feel good about your day.

Is this because one organization is focused on Net Promoter Scores, is it lack of choice in the phone marketplace?  Is it that they’re sitting in a call center and have dealt with thousands of customers on a day-by-day basis?  Or is it just training?

At Northstar we know you have a choice, we know that we want you back next year, we know we want you to bring your friends, or encourage them to take lessons (or anything else).  We want you to not be just satisfied but delighted.  AT&T on the other hand is just there to make sure you’re satisfied, which means that when I ask if I can have the shipping expedited you can only say “the system won’t let me do that”.

What does this mean to me, or to you the reader?  It’s easy to make the customer happy, it’s easy to delight a customer through interactions, make sure your employees are empowered to make it happen.

 

SMTP Client for Tornado

Was looking around for a mail handler for Tornado, found it pretty amazing that it didn’t exist.  Since I’ve only written on commercial SMTP server and have been playing with just about everything in Tornado Web at this point I figured it wasn’t too much work to whip one out.  So, instead of using annoying little threads, here’s a fully async smtp client for Tornado.

Nothing fancy, just gets the job done.

Also available as a GitHub gist - https://gist.github.com/1358253

</span>
<pre>from tornado import ioloop
from tornado import iostream
import socket

class Envelope(object):
    def __init__(self, sender, rcpt, body, callback):
        self.sender = sender
        self.rcpt   = rcpt[:]
        self.body   = body
        self.callback = callback

class SMTPClient(object):
    CLOSED = -2
    CONNECTED = -1
    IDLE = 0
    EHLO = 1
    MAIL = 2
    RCPT = 3
    DATA = 4
    DATA_DONE = 5
    QUIT = 6

    def __init__(self, host='localhost', port=25):
        self.host = host
        self.port = port
        self.msgs = []
        self.stream = None
        self.state = self.CLOSED

    def send_message(self, msg, callback=None):
        """Message is a django style EmailMessage object"""

        if not msg:
            return

        self.msgs.append(Envelope(msg.from_email, msg.recipients(), msg.message().as_string(), callback))

        self.begin()

    def send(self, sender=None, rcpt=[], body="", callback=None):
        """Very simple sender, just take the necessary parameters to create an envelope"""
        self.msgs.append(Envelope(sender, rcpt, body, callback))

        self.begin()

    def begin(self):
        """Start the sending of a message, if we need a connection open it"""
        if not self.stream:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
            self.stream = iostream.IOStream(s)
            self.stream.connect((self.host, self.port), self.connected)
        else:
            self.work_or_quit(self.process)

    def work_or_quit(self, callback=None):
        """
           callback is provided, for the startup case where we're not in the main processing loop
        """
        if self.state == self.IDLE:
            if self.msgs:
                self.state = self.MAIL
                self.stream.write('MAIL FROM: <%s>\r\n' % self.msgs[0].sender)
            else:
                self.state = self.QUIT
                self.stream.write('QUIT\r\n')
            if callback:
                self.stream.read_until('\r\n', callback)

    def connected(self):
        """Socket connect callback"""
        self.state = self.CONNECTED
        self.stream.read_until('\r\n', self.process)

    def process(self, data):
        # print self.state, data,
        code = int(data[0:3])
        if data[3] not in (' ', '\r', '\n'):
            self.stream.read_until('\r\n', self.process)
            return

        if self.state == self.CONNECTED:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from CONNECT: %s" % (code, data.strip()))
            self.state = self.EHLO
            self.stream.write('EHLO localhost\r\n')
        elif self.state == self.EHLO:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from EHLO: %s" % (code, data.strip()))
            self.state = self.IDLE
            self.work_or_quit()
        elif self.state == self.MAIL:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from MAIL: %s" % (code, data.strip()))
            if self.msgs[0].rcpt:
                self.stream.write('RCPT TO: <%s>\r\n' % self.msgs[0].rcpt.pop(0))
            self.state = self.RCPT
        elif self.state == self.RCPT:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from RCPT: %s" % (code, data.strip()))
            if self.msgs[0].rcpt:
                self.stream.write('RCPT TO: <%s>\r\n' % self.msgs[0].rcpt.pop(0))
            else:
                self.stream.write('DATA\r\n')
                self.state = self.DATA
        elif self.state == self.DATA:
            if code not in (354,) :
                return self.error("Unexpected status %d from DATA: %s" % (code, data.strip()))
            self.stream.write(self.msgs[0].body)
            if self.msgs[0].body[-2:] != '\r\n':
                self.stream.write('\r\n')
            self.stream.write('.\r\n')
            self.state = self.DATA_DONE
        elif self.state == self.DATA_DONE:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from DATA END: %s" % (code, data.strip()))
            if self.msgs[0].callback:
                self.msgs[0].callback(True)

            self.msgs.pop(0)

            self.state = self.IDLE
            self.work_or_quit()
        elif self.state == self.QUIT:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from QUIT: %s" % (code, data.strip()))
            self.close()

        if self.stream:
            self.stream.read_until('\r\n', self.process)

    def error(self, msg):
        self.close()

    def close(self):
        for msg in self.msgs:
            if msg.callback:
                msg.callback(False)
        self.stream.close()
        self.stream = None
        self.state = self.CLOSED

if __name__ == '__main__':
    client = SMTPClient('localhost', 25)
    body = """Subject: Testing

Just a test
    """
    client.send('foo@example.com', ['recipient@example.com'], body)
    ioloop.IOLoop.instance().start()

Python Lazy Object Reloader

I’m sure there’s a more general title for this kind of object, but the challenge came up and here’s what I put together.  The basic idea is to have a python class that only recomputes something hard when one of the attributes is modified.  It doesn’t make sense to recompute on every modification, nor does it make sense to recompute if it’s never read.

In all cases, here’s the code sample.  I’m sure it’ll be useful on stackoverflow someday.

 

#
#  Decorator that takes a list of members that are cached
#
def second_cache(members=None) :
    def inner(cls) :
        cls._second_cache_dirty = False

        if members is None :
            def setter(self, name, value) :
                if name not in ('_second_cache_dirty', ) :
                    self._second_cache_dirty = True
                return object.__setattr__(self, name, value)
        else :
            cls._second_cache_members = set(members)
            def setter(self, name, value) :
                if name in cls._second_cache_members and name not in ('_second_cache_dirty',):
                    self._second_cache_dirty = True
                return object.__setattr__(self, name, value)

        cls.__setattr__ = setter

        return cls
    return inner

#
#  A revised property decorator that checkes the cacheable state of an object and
#   calls refresh if defined and needed
#
def cache_prop(func) :
    def call_me(self) :
        if self._second_cache_dirty :
            if hasattr(self, '_refresh') :
                self._refresh()
                self._second_cache_dirty = False
        return func(self)
    return property(call_me)

#
# Test class to demonstrate that only if the member bar is modified that the object
#  calls the refresh method.
#
@second_cache(members=['bar'])
class Test(object) :
    def __init__(self) :
        self.bar = 1
        self.cat = 2
        self.dog = 3
        self.baz = 4

    def _refresh(self) :
        print "Cache Dirty - need to refresh object"

    @cache_prop
    def frog(self) :
        return self.bar + self.cat

    def fun(self) :
        print "BigTop"

t1 = Test()

print "Dirty:", t1.frog
t1.bar = 7
print "Dirty:", t1.frog
print "Clean:", t1.frog


 

Tagged

Can Amazon be great!

Recently read this great little article about doing map reduce over music.  What was interesting is that the author put up a test dataset for people to use, but of course had to caveat it with “don’t abuse”.   What’s interesting is that there is a host of interesting datasets out there:

  • Music
  • Wikipedia
  • Some IMDB information (movies)
  • …others…
Could amazon be great and construct a repository for this data so everybody from student researchers to people playing around with the next great startup idea could have access to this information.  After all, how many times do people want to start their project with the following steps:  Crawl/Download; Format/Extract; … then finally process.
Be great!  Offer up a no-cost (or very..very… low cost) access to a bunch of shared data.

“You are not your customer” – organizational insult

One of the motos that has gotten embedded in corporate culture is “You are not your customer” as an engineer I see this all the time.  A designer builds something, engineering looks at it and says “but…”.  A manager responds with the flip, “The designer is the professional, you’re not the customer”.

With an attitude like that the message is abdicate your responsibility and focus on your job.  How can you build as successful company if all you do is your job, we need to think outside our role, challenge assumptions and build great products.  Since this is Steve Jobs week, he is noted for playing many roles he challenges the assumption that “you are not your customer”.  He is the customer he cares, if I’m building any product I need to put on my customer hat and think what I’m looking for.

How do you challenge “your are not your customer”?  You don’t need to challenge it, you need to demonstrate a care for the product, understand the use (and user) and live in their shoes.  It’s a form of empathy, we’ve been wired for thousands of years to care.  Bring up your experience, your understanding, your passion for great products.

Framing your Customer

We were working on some customer issues the other day and it another customer was having problems, you know double whammy.  What was interesting is that a more senior person started talking about the customer about “they’re such a problem” and then talking about strange things they did in the past.  As we dug into their problem, yes they were using an API in a very strange way (20 seconds of runtime to yield 6 results), but what was more interesting is that the more junior people started talking about how this customer was bad/wrong – taking on the way leadership has framed them.

In software we have an expectation of how somebody is going to do things, but we don’t have the “customer is always right” ideals that Customer Service/Sales takes to heart.  As wiser leaders we need to make sure that we frame how we talk about customers in ways that help people understand the basics of a customer centered development model (they’re always right).  Since regardless of what we think, it is what they want to do which keeps them happy and keeps things going forward.  Not only that but as we start to understand what they want to do, only then can we build systems that truly support their needs.

There is a whole different discussion about firing your customers, but thats an explicit option, not a cultural issue.